Skip to content

Commit 6e682b1

Browse files
authored
Merge pull request #90 from yetanalytics/LRS-30_2_0_compat_dynamic
xAPI 2.0.0 Support
2 parents f045cdb + ad78a2b commit 6e682b1

4 files changed

Lines changed: 225 additions & 34 deletions

File tree

src/xapi_schema/spec.cljc

Lines changed: 138 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
MailToIRIRegEx
88
UuidRegEx
99
TimestampRegEx
10+
TimestampRegEx200
1011
xAPIVersionRegEx
12+
xAPIVersionRegEx200
1113
DurationRegEx
14+
DurationRegEx200
1215
Sha1RegEx
1316
Sha2RegEx]]
1417
[clojure.spec.alpha :as s #?@(:cljs [:include-macros true])]
@@ -22,6 +25,10 @@
2225
"When true, coerce 0.95 context activities to conform."
2326
true)
2427

28+
(def ^:dynamic *xapi-version*
29+
"xAPI Statement Version to Conform"
30+
"1.0.3")
31+
2532
;; Utils
2633

2734
(def double-conformer
@@ -234,7 +241,11 @@
234241
[timestamp]
235242
(letfn [(parse-int [s] #?(:clj (Integer/parseInt s) :cljs (js/parseInt s)))]
236243
(let [[ts year month day _hour _min _sec _sec-frac _offset]
237-
(re-matches TimestampRegEx timestamp)
244+
(re-matches
245+
(case *xapi-version*
246+
"1.0.3" TimestampRegEx
247+
"2.0.0" TimestampRegEx200)
248+
timestamp)
238249
month-int (when month (parse-int month))
239250
year-int (when year (parse-int year))
240251
day-int (when day (parse-int day))]
@@ -269,7 +280,11 @@
269280
(s/def ::duration
270281
(s/with-gen
271282
(s/and string?
272-
(partial re-matches DurationRegEx))
283+
#(re-matches
284+
(case *xapi-version*
285+
"1.0.3" DurationRegEx
286+
"2.0.0" DurationRegEx200)
287+
%))
273288
#(sgen/fmap (fn [[h m s]]
274289
(#?(:clj format
275290
:cljs gstring/format) "PT%dH%sM%dS" h m s))
@@ -280,11 +295,12 @@
280295
(s/def ::version
281296
(s/with-gen
282297
(s/and string?
283-
(partial re-matches xAPIVersionRegEx))
284-
#(sgen/fmap (fn [i]
285-
(#?(:clj format
286-
:cljs gstring/format) "1.0.%d" i))
287-
(sgen/int))))
298+
#(re-matches
299+
(case *xapi-version*
300+
"1.0.3" xAPIVersionRegEx
301+
"2.0.0" xAPIVersionRegEx200)
302+
%))
303+
#(sgen/return *xapi-version*)))
288304

289305
(s/def ::sha2
290306
(s/with-gen
@@ -815,17 +831,17 @@
815831
(-> scores
816832
(assoc :score/min raw)
817833
(assoc :score/raw min))
818-
834+
819835
(and min max (< max min))
820836
(-> scores
821837
(assoc :score/min max)
822838
(assoc :score/max min))
823-
839+
824840
(and raw max (< max raw))
825841
(-> scores
826842
(assoc :score/raw max)
827843
(assoc :score/max raw))
828-
844+
829845
:else
830846
scores))
831847

@@ -973,27 +989,107 @@
973989
(s/def :context/extensions
974990
::extensions)
975991

976-
(s/def ::context
977-
(conform-ns "context"
992+
;; 2.0.x compat
993+
994+
;; contextAgents
995+
(s/def :contextAgent/objectType #{"contextAgent"})
996+
(s/def :contextAgent/agent ::agent)
997+
(s/def :contextAgent/relevantTypes
998+
(s/every ::iri
999+
:into []
1000+
:min-count 1))
1001+
1002+
(s/def ::context-agent
1003+
(conform-ns "contextAgent"
1004+
(s/and
1005+
(s/keys :req [:contextAgent/objectType
1006+
:contextAgent/agent]
1007+
:opt [:contextAgent/relevantTypes])
1008+
(restrict-keys :contextAgent/objectType
1009+
:contextAgent/agent
1010+
:contextAgent/relevantTypes))))
1011+
(s/def :context/contextAgents
1012+
(s/every ::context-agent
1013+
:into []))
1014+
1015+
;; contextGroups
1016+
1017+
(s/def :contextGroup/objectType #{"contextGroup"})
1018+
(s/def :contextGroup/group ::group)
1019+
(s/def :contextGroup/relevantTypes
1020+
(s/every ::iri
1021+
:into []
1022+
:min-count 1))
1023+
1024+
(s/def ::context-group
1025+
(conform-ns "contextGroup"
9781026
(s/and
979-
(s/keys :opt [:context/registration
980-
:context/instructor
981-
:context/team
982-
:context/contextActivities
983-
:context/revision
984-
:context/platform
985-
:context/language
986-
:context/statement
987-
:context/extensions])
988-
(restrict-keys :context/registration
989-
:context/instructor
990-
:context/team
991-
:context/contextActivities
992-
:context/revision
993-
:context/platform
994-
:context/language
995-
:context/statement
996-
:context/extensions))))
1027+
(s/keys :req [:contextGroup/objectType
1028+
:contextGroup/group]
1029+
:opt [:contextGroup/relevantTypes])
1030+
(restrict-keys :contextGroup/objectType
1031+
:contextGroup/group
1032+
:contextGroup/relevantTypes))))
1033+
(s/def :context/contextGroups
1034+
(s/every ::context-group
1035+
:into []))
1036+
1037+
;; multispec for dynamic params
1038+
(defmulti context-version (fn [_] *xapi-version*))
1039+
1040+
(defmethod context-version "1.0.3" [_]
1041+
(conform-ns
1042+
"context"
1043+
(s/and
1044+
(s/keys :opt [:context/registration
1045+
:context/instructor
1046+
:context/team
1047+
:context/contextActivities
1048+
:context/revision
1049+
:context/platform
1050+
:context/language
1051+
:context/statement
1052+
:context/extensions])
1053+
(restrict-keys :context/registration
1054+
:context/instructor
1055+
:context/team
1056+
:context/contextActivities
1057+
:context/revision
1058+
:context/platform
1059+
:context/language
1060+
:context/statement
1061+
:context/extensions))))
1062+
1063+
(defmethod context-version "2.0.0" [_]
1064+
(conform-ns
1065+
"context"
1066+
(s/and
1067+
(s/keys :opt [:context/registration
1068+
:context/instructor
1069+
:context/team
1070+
:context/contextActivities
1071+
:context/revision
1072+
:context/platform
1073+
:context/language
1074+
:context/statement
1075+
:context/extensions
1076+
:context/contextAgents
1077+
:context/contextGroups])
1078+
(restrict-keys :context/registration
1079+
:context/instructor
1080+
:context/team
1081+
:context/contextActivities
1082+
:context/revision
1083+
:context/platform
1084+
:context/language
1085+
:context/statement
1086+
:context/extensions
1087+
:context/contextAgents
1088+
:context/contextGroups))))
1089+
1090+
(s/def ::context
1091+
(s/multi-spec context-version (fn [gen-val _]
1092+
gen-val) ))
9971093

9981094
;; Attachments
9991095

@@ -1326,8 +1422,19 @@
13261422
(some-> s :statement/object :statement-ref/objectType)
13271423
true)))))
13281424

1425+
(defn unique-statement-ids?
1426+
"Spec predicate to ensure that the IDs of a list of statements are unique."
1427+
[statements]
1428+
(let [ids (keep #(get % "id") statements)]
1429+
(or
1430+
(empty? ids)
1431+
(reduce distinct? ids)
1432+
::s/invalid)))
1433+
13291434
(s/def ::statements
1330-
(s/coll-of ::statement :into []))
1435+
(s/and
1436+
(s/coll-of ::statement :into [])
1437+
unique-statement-ids?))
13311438

13321439
(s/def ::lrs-statements
13331440
(s/coll-of ::lrs-statement :into []))

src/xapi_schema/spec/regex.cljc

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,60 @@
144144
dur-week)]
145145
(re-pattern (str "^P(?:" duration ")|P(?:" (base-timestamp) ")$"))))
146146

147+
(defn- base-timestamp-200 []
148+
(let [;; Date
149+
year "(\\d{4})"
150+
month "(0[1-9]|1[0-2])"
151+
day "(0[1-9]|[12]\\d|3[01])" ; ignore month/leap year constraints
152+
;; Time
153+
hour "([01]\\d|2[0-3])"
154+
min "([0-5]\\d)"
155+
sec "([0-5]\\d|60)" ; leap seconds
156+
sec-frac "(\\.\\d+)"
157+
;; Time
158+
time (str "(?:" hour ":" min ":" sec sec-frac "?" ")")
159+
date (str "(?:" year "-" month "-" day ")")]
160+
(str date "[T\\s]" time)))
161+
162+
(def TimestampRegEx200 ; RFC 3339
163+
(let [;; Time
164+
hour "(?:[01]\\d|2[0-3])"
165+
min "(?:[0-5]\\d)"
166+
;; Offset
167+
lookahead "(?!-00:00)"
168+
num-offset (str "(?:[+-]" hour ":" min ")")
169+
time-offset (str "(Z|" lookahead num-offset ")")]
170+
(re-pattern (str "^" (base-timestamp-200) time-offset "$"))))
171+
172+
(def DurationRegEx200 ; ISO 8601 Durations
173+
(let [dy "(?:\\d+Y|\\d+\\.\\d+Y$)"
174+
dm "(?:\\d+M|\\d+\\.\\d+M$)"
175+
dw "(?:\\d+W|\\d+\\.\\d+W$)"
176+
dd "(?:\\d+D|\\d+\\.\\d+D$)"
177+
dh "(?:\\d+H|\\d+\\.\\d+H$)"
178+
ds "(?:\\d+S|\\d+\\.\\d+S$)"
179+
dur-date (str "(?:" dd "|" dm dd "?" "|" dy dm "?" dd "?" ")")
180+
dur-time (str "(?:" ds "|" dm ds "?" "|" dh dm "?" ds "?" ")")
181+
dur-week (str "(?:" dw ")")
182+
duration (str "(?:" dur-date "(?:T" dur-time ")?" ")" "|"
183+
"(?:T" dur-time ")" "|"
184+
dur-week)]
185+
(re-pattern (str "^P(?:" duration ")|P(?:" (base-timestamp-200) ")$"))))
186+
187+
147188
;; Based on http://www.regexr.com/39s32
148189
(def xAPIVersionRegEx
149190
(let [suf-part "[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*"
150191
suffix (str "(\\.[0-9]+(?:-" suf-part ")?(?:\\+" suf-part ")?)?")
151192
ver-str (str "^1\\.0" suffix "$")]
152193
(re-pattern ver-str)))
153194

195+
(def xAPIVersionRegEx200
196+
(let [suf-part "[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*"
197+
suffix (str "(\\.[0-9]+(?:-" suf-part ")?(?:\\+" suf-part ")?)?")
198+
ver-str (str "^(1\\.0" suffix ")|(2\\.0\\.0)$")]
199+
(re-pattern ver-str)))
200+
154201
(def Base64RegEx
155202
(let [fs #?(:clj "\\/" :cljs "/")
156203
body (str "(?:[A-Za-z0-9\\+" fs "]{4})*")

test/xapi_schema/spec/regex_test.cljc

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
MailToIRIRegEx
1010
UuidRegEx
1111
TimestampRegEx
12+
TimestampRegEx200
1213
xAPIVersionRegEx
14+
xAPIVersionRegEx200
1315
DurationRegEx
1416
Base64RegEx
1517
Sha1RegEx
@@ -145,15 +147,20 @@
145147
(is (not (re-matches TimestampRegEx "20150513T15Z")))
146148
(is (not (re-matches TimestampRegEx "20150513T15:16:00Z")))
147149
;; negative offset
148-
(is (not (re-matches TimestampRegEx "2008-09-15T15:53:00.601-00:00")))))
150+
(is (not (re-matches TimestampRegEx "2008-09-15T15:53:00.601-00:00"))))
151+
(testing "matches valid but terrible stamps in rfc3339 OUTSIDE of 8601"
152+
(is (re-matches TimestampRegEx200 "2015-05-13 15:16:00Z"))))
149153

150154
(deftest xapi-version-regex-test
151155
(testing "matches xAPI 1.0.X versions"
152156
(is (and (re-matches xAPIVersionRegEx "1.0.0")
153157
(re-matches xAPIVersionRegEx "1.0.2")
154158
(re-matches xAPIVersionRegEx "1.0")
155159
(re-matches xAPIVersionRegEx "1.0.32-abc.def+ghi.jkl")))
156-
(is (not (re-matches xAPIVersionRegEx "0.9.5")))))
160+
(is (not (re-matches xAPIVersionRegEx "0.9.5"))))
161+
(testing "matches xAPI 2.0.0 version only"
162+
(is (and (re-matches xAPIVersionRegEx200 "2.0.0")
163+
(not (re-matches xAPIVersionRegEx200 "2.0.2"))))))
157164

158165
(deftest duration-regex-test
159166
(testing "matches ISO durations"

test/xapi_schema/spec_test.cljc

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,37 @@
389389
:bad
390390
{"team" {"mbox" "mailto:a@b.com"}}
391391
{"team" {"mbox" "mailto:a@b.com"
392-
"objectType" "Agent"}}))))
392+
"objectType" "Agent"}})))
393+
(testing "xAPI 2.0.0"
394+
(binding [xs/*xapi-version* "2.0.0"]
395+
(testing "contextAgents"
396+
(should-satisfy+
397+
::xs/context
398+
{"contextAgents"
399+
[{:objectType "contextAgent"
400+
:agent {"mbox" "mailto:a@b.com"
401+
"objectType" "Agent"}}]}
402+
:bad
403+
{"contextAgents" [{"mbox" "mailto:a@b.com"
404+
"objectType" "Agent"}]}
405+
{"contextAgents"
406+
[{:objectType "contextGroup"
407+
:group {"mbox" "mailto:a@b.com"
408+
"objectType" "Group"}}]}))
409+
(testing "contextGroups"
410+
(should-satisfy+
411+
::xs/context
412+
{"contextGroups"
413+
[{:objectType "contextGroup"
414+
:group {"mbox" "mailto:a@b.com"
415+
"objectType" "Group"}}]}
416+
:bad
417+
{"contextGroups" [{"mbox" "mailto:a@b.com"
418+
"objectType" "Group"}]}
419+
{"contextGroups"
420+
[{:objectType "contextAgent"
421+
:agent {"mbox" "mailto:a@b.com"
422+
"objectType" "Agent"}}]})))))
393423

394424
(deftest attachment-test
395425
(testing

0 commit comments

Comments
 (0)