From 2427b02b21b3c09584b63a8e6e66d39810325f8c Mon Sep 17 00:00:00 2001 From: Milt Reder Date: Wed, 5 May 2021 15:29:44 -0400 Subject: [PATCH 1/9] Allow 2.0.x versions --- src/xapi_schema/spec/regex.cljc | 4 ++-- test/xapi_schema/spec/regex_test.cljc | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/xapi_schema/spec/regex.cljc b/src/xapi_schema/spec/regex.cljc index 5bb00ea..c1bc173 100644 --- a/src/xapi_schema/spec/regex.cljc +++ b/src/xapi_schema/spec/regex.cljc @@ -1,7 +1,7 @@ (ns xapi-schema.spec.regex (:require [clojure.string :refer [join]])) -(def LanguageTagRegEx ; RFC 5646, w/ lang subtag limitation +(def LanguageTagRegEx ; RFC 5646, w/ lang subtag limitation (let [;; Language Subtags ;; Note: we exclude 4-8 char subtags, even though they are allowed in ;; the RFC spec, since they are reserved for future (not current) use. @@ -148,7 +148,7 @@ (def xAPIVersionRegEx (let [suf-part "[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*" suffix (str "(\\.[0-9]+(?:-" suf-part ")?(?:\\+" suf-part ")?)?") - ver-str (str "^1\\.0" suffix "$")] + ver-str (str "^[1-2]\\.0" suffix "$")] (re-pattern ver-str))) (def Base64RegEx diff --git a/test/xapi_schema/spec/regex_test.cljc b/test/xapi_schema/spec/regex_test.cljc index b9fa0cc..dfac17c 100644 --- a/test/xapi_schema/spec/regex_test.cljc +++ b/test/xapi_schema/spec/regex_test.cljc @@ -148,7 +148,12 @@ (re-matches xAPIVersionRegEx "1.0.2") (re-matches xAPIVersionRegEx "1.0") (re-matches xAPIVersionRegEx "1.0.32-abc.def+ghi.jkl"))) - (is (not (re-matches xAPIVersionRegEx "0.9.5"))))) + (is (not (re-matches xAPIVersionRegEx "0.9.5")))) + (testing "matches xAPI 2.0.x versions" + (is (and (re-matches xAPIVersionRegEx "2.0.0") + (re-matches xAPIVersionRegEx "2.0.2") + (re-matches xAPIVersionRegEx "2.0") + (re-matches xAPIVersionRegEx "2.0.32-abc.def+ghi.jkl"))))) (deftest duration-regex-test (testing "matches ISO durations" From dc64ea9fc487a09702ba81acf2c184a816e0e04e Mon Sep 17 00:00:00 2001 From: Milt Reder Date: Wed, 5 May 2021 16:36:56 -0400 Subject: [PATCH 2/9] LRS-30 naive addition of context agents, groups --- src/xapi_schema/spec.cljc | 53 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/src/xapi_schema/spec.cljc b/src/xapi_schema/spec.cljc index 226bc0b..7cdafd4 100644 --- a/src/xapi_schema/spec.cljc +++ b/src/xapi_schema/spec.cljc @@ -949,6 +949,51 @@ (s/def :context/extensions ::extensions) +;; 2.0.x compat + +;; contextAgents +(s/def :contextAgent/objectType #{"contextAgent"}) +(s/def :contextAgent/agent ::agent) +(s/def :contextAgent/relevantTypes + (s/every ::iri + :into [] + :min-count 1)) + +(s/def ::context-agent + (conform-ns "contextAgent" + (s/and + (s/keys :req [:contextAgent/objectType + :contextAgent/agent] + :opt [:contextAgent/relevantTypes]) + (restrict-keys :contextAgent/objectType + :contextAgent/agent + :contextAgent/relevantTypes)))) +(s/def :context/contextAgents + (s/every ::context-agent + :into [])) + +;; contextGroups + +(s/def :contextGroup/objectType #{"contextGroup"}) +(s/def :contextGroup/group ::group) +(s/def :contextGroup/relevantTypes + (s/every ::iri + :into [] + :min-count 1)) + +(s/def ::context-group + (conform-ns "contextGroup" + (s/and + (s/keys :req [:contextGroup/objectType + :contextGroup/group] + :opt [:contextGroup/relevantTypes]) + (restrict-keys :contextGroup/objectType + :contextGroup/group + :contextGroup/relevantTypes)))) +(s/def :context/contextGroups + (s/every ::context-group + :into [])) + (s/def ::context (conform-ns "context" (s/and @@ -960,7 +1005,9 @@ :context/platform :context/language :context/statement - :context/extensions]) + :context/extensions + :context/contextAgents + :context/contextGroups]) (restrict-keys :context/registration :context/instructor :context/team @@ -969,7 +1016,9 @@ :context/platform :context/language :context/statement - :context/extensions)))) + :context/extensions + :context/contextAgents + :context/contextGroups)))) ;; Attachments From 9a13ae68cc7b562078bf5e56bb29c8b479c11a41 Mon Sep 17 00:00:00 2001 From: Milt Reder Date: Wed, 5 May 2021 17:03:50 -0400 Subject: [PATCH 3/9] LRS-30 Allow whitespace instead of T per RFC 3339 I guess --- src/xapi_schema/spec/regex.cljc | 2 +- test/xapi_schema/spec/regex_test.cljc | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/xapi_schema/spec/regex.cljc b/src/xapi_schema/spec/regex.cljc index c1bc173..69ad89c 100644 --- a/src/xapi_schema/spec/regex.cljc +++ b/src/xapi_schema/spec/regex.cljc @@ -117,7 +117,7 @@ ;; Time time (str "(?:" hour ":" min ":" sec sec-frac "?" ")") date (str "(?:" year "-" month "-" day ")")] - (str date "T" time))) + (str date "[T\\s]" time))) (def TimestampRegEx ; RFC 3339 (let [;; Time diff --git a/test/xapi_schema/spec/regex_test.cljc b/test/xapi_schema/spec/regex_test.cljc index dfac17c..60556c4 100644 --- a/test/xapi_schema/spec/regex_test.cljc +++ b/test/xapi_schema/spec/regex_test.cljc @@ -140,7 +140,9 @@ (is (not (re-matches TimestampRegEx "20150513T15Z"))) (is (not (re-matches TimestampRegEx "20150513T15:16:00Z"))) ;; negative offset - (is (not (re-matches TimestampRegEx "2008-09-15T15:53:00.601-00:00"))))) + (is (not (re-matches TimestampRegEx "2008-09-15T15:53:00.601-00:00")))) + (testing "matches valid but terrible stamps in rfc3339 OUTSIDE of 8601" + (is (re-matches TimestampRegEx "2015-05-13 15:16:00Z")))) (deftest xapi-version-regex-test (testing "matches xAPI 1.0.X versions" From 92876f9e7c9d0df69db27c7e268d9517a880591b Mon Sep 17 00:00:00 2001 From: Milt Reder Date: Wed, 5 May 2021 19:15:02 -0400 Subject: [PATCH 4/9] LRS-30 for 2.0.0+, version is JUST 2.0.0 --- src/xapi_schema/spec.cljc | 5 +---- src/xapi_schema/spec/regex.cljc | 2 +- test/xapi_schema/spec/regex_test.cljc | 6 ++---- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/xapi_schema/spec.cljc b/src/xapi_schema/spec.cljc index 7cdafd4..8531479 100644 --- a/src/xapi_schema/spec.cljc +++ b/src/xapi_schema/spec.cljc @@ -288,10 +288,7 @@ (s/with-gen (s/and string? (partial re-matches xAPIVersionRegEx)) - #(sgen/fmap (fn [i] - (#?(:clj format - :cljs gstring/format) "1.0.%d" i)) - (sgen/int)))) + #(sgen/return "2.0.0"))) (s/def ::sha2 (s/with-gen diff --git a/src/xapi_schema/spec/regex.cljc b/src/xapi_schema/spec/regex.cljc index 69ad89c..39f0665 100644 --- a/src/xapi_schema/spec/regex.cljc +++ b/src/xapi_schema/spec/regex.cljc @@ -148,7 +148,7 @@ (def xAPIVersionRegEx (let [suf-part "[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*" suffix (str "(\\.[0-9]+(?:-" suf-part ")?(?:\\+" suf-part ")?)?") - ver-str (str "^[1-2]\\.0" suffix "$")] + ver-str (str "^(1\\.0" suffix ")|(2\\.0\\.0)$")] (re-pattern ver-str))) (def Base64RegEx diff --git a/test/xapi_schema/spec/regex_test.cljc b/test/xapi_schema/spec/regex_test.cljc index 60556c4..493fd0b 100644 --- a/test/xapi_schema/spec/regex_test.cljc +++ b/test/xapi_schema/spec/regex_test.cljc @@ -151,11 +151,9 @@ (re-matches xAPIVersionRegEx "1.0") (re-matches xAPIVersionRegEx "1.0.32-abc.def+ghi.jkl"))) (is (not (re-matches xAPIVersionRegEx "0.9.5")))) - (testing "matches xAPI 2.0.x versions" + (testing "matches xAPI 2.0.0 version only" (is (and (re-matches xAPIVersionRegEx "2.0.0") - (re-matches xAPIVersionRegEx "2.0.2") - (re-matches xAPIVersionRegEx "2.0") - (re-matches xAPIVersionRegEx "2.0.32-abc.def+ghi.jkl"))))) + (not (re-matches xAPIVersionRegEx "2.0.2")))))) (deftest duration-regex-test (testing "matches ISO durations" From 184fbf370b434b3f3a1e36e25ebdd7b50d257bfc Mon Sep 17 00:00:00 2001 From: Milton Reder Date: Thu, 21 Aug 2025 15:43:48 -0400 Subject: [PATCH 5/9] ensure unique ids --- src/xapi_schema/spec.cljc | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/xapi_schema/spec.cljc b/src/xapi_schema/spec.cljc index 9409635..8decbf9 100644 --- a/src/xapi_schema/spec.cljc +++ b/src/xapi_schema/spec.cljc @@ -1372,8 +1372,16 @@ (some-> s :statement/object :statement-ref/objectType) true))))) +(defn unique-statement-ids? + "Spec predicate to ensure that the IDs of a list of statements are unique." + [statements] + (or (distinct? (map #(get % "id") statements)) + ::s/invalid)) + (s/def ::statements - (s/coll-of ::statement :into [])) + (s/and + (s/coll-of ::statement :into []) + unique-statement-ids?)) (s/def ::lrs-statements (s/coll-of ::lrs-statement :into [])) From 822e66bb291a8821e4abc476bf2319cc218802df Mon Sep 17 00:00:00 2001 From: Milton Reder Date: Thu, 21 Aug 2025 15:53:22 -0400 Subject: [PATCH 6/9] fix arity of call to distinct? --- src/xapi_schema/spec.cljc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xapi_schema/spec.cljc b/src/xapi_schema/spec.cljc index 8decbf9..c8da52f 100644 --- a/src/xapi_schema/spec.cljc +++ b/src/xapi_schema/spec.cljc @@ -1375,7 +1375,7 @@ (defn unique-statement-ids? "Spec predicate to ensure that the IDs of a list of statements are unique." [statements] - (or (distinct? (map #(get % "id") statements)) + (or (reduce distinct? (map #(get % "id") statements)) ::s/invalid)) (s/def ::statements From 4164f959cbf844c194aa4705e41928ed89b9f9d7 Mon Sep 17 00:00:00 2001 From: Milton Reder Date: Fri, 22 Aug 2025 10:06:59 -0400 Subject: [PATCH 7/9] guard against empty list passed to distinct --- src/xapi_schema/spec.cljc | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/xapi_schema/spec.cljc b/src/xapi_schema/spec.cljc index c8da52f..4a536ba 100644 --- a/src/xapi_schema/spec.cljc +++ b/src/xapi_schema/spec.cljc @@ -812,17 +812,17 @@ (-> scores (assoc :score/min raw) (assoc :score/raw min)) - + (and min max (< max min)) (-> scores (assoc :score/min max) (assoc :score/max min)) - + (and raw max (< max raw)) (-> scores (assoc :score/raw max) (assoc :score/max raw)) - + :else scores)) @@ -1375,8 +1375,11 @@ (defn unique-statement-ids? "Spec predicate to ensure that the IDs of a list of statements are unique." [statements] - (or (reduce distinct? (map #(get % "id") statements)) - ::s/invalid)) + (or + (empty? statements) + (reduce distinct? + (map #(get % "id") statements)) + ::s/invalid)) (s/def ::statements (s/and From 6e06f8591a330d981d5532224c8055b623d89eae Mon Sep 17 00:00:00 2001 From: Milton Reder Date: Fri, 22 Aug 2025 10:09:22 -0400 Subject: [PATCH 8/9] properly handle missing ids --- src/xapi_schema/spec.cljc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/xapi_schema/spec.cljc b/src/xapi_schema/spec.cljc index 4a536ba..9757d79 100644 --- a/src/xapi_schema/spec.cljc +++ b/src/xapi_schema/spec.cljc @@ -1375,11 +1375,11 @@ (defn unique-statement-ids? "Spec predicate to ensure that the IDs of a list of statements are unique." [statements] - (or - (empty? statements) - (reduce distinct? - (map #(get % "id") statements)) - ::s/invalid)) + (let [ids (keep #(get % "id") statements)] + (or + (empty? ids) + (reduce distinct? ids) + ::s/invalid))) (s/def ::statements (s/and From ad78a2ba5fa55dba1fd6cc08005bd116cfa6f6c4 Mon Sep 17 00:00:00 2001 From: Milton Reder Date: Mon, 25 Aug 2025 11:12:30 -0400 Subject: [PATCH 9/9] dynamic var for xapi version --- src/xapi_schema/spec.cljc | 106 +++++++++++++++++++------- src/xapi_schema/spec/regex.cljc | 49 +++++++++++- test/xapi_schema/spec/regex_test.cljc | 8 +- test/xapi_schema/spec_test.cljc | 32 +++++++- 4 files changed, 162 insertions(+), 33 deletions(-) diff --git a/src/xapi_schema/spec.cljc b/src/xapi_schema/spec.cljc index 9757d79..84ccc1b 100644 --- a/src/xapi_schema/spec.cljc +++ b/src/xapi_schema/spec.cljc @@ -7,8 +7,11 @@ MailToIRIRegEx UuidRegEx TimestampRegEx + TimestampRegEx200 xAPIVersionRegEx + xAPIVersionRegEx200 DurationRegEx + DurationRegEx200 Sha1RegEx Sha2RegEx]] [clojure.spec.alpha :as s #?@(:cljs [:include-macros true])] @@ -22,6 +25,10 @@ "When true, coerce 0.95 context activities to conform." true) +(def ^:dynamic *xapi-version* + "xAPI Statement Version to Conform" + "1.0.3") + ;; Utils (def double-conformer @@ -234,7 +241,11 @@ [timestamp] (letfn [(parse-int [s] #?(:clj (Integer/parseInt s) :cljs (js/parseInt s)))] (let [[ts year month day _hour _min _sec _sec-frac _offset] - (re-matches TimestampRegEx timestamp) + (re-matches + (case *xapi-version* + "1.0.3" TimestampRegEx + "2.0.0" TimestampRegEx200) + timestamp) month-int (when month (parse-int month)) year-int (when year (parse-int year)) day-int (when day (parse-int day))] @@ -269,7 +280,11 @@ (s/def ::duration (s/with-gen (s/and string? - (partial re-matches DurationRegEx)) + #(re-matches + (case *xapi-version* + "1.0.3" DurationRegEx + "2.0.0" DurationRegEx200) + %)) #(sgen/fmap (fn [[h m s]] (#?(:clj format :cljs gstring/format) "PT%dH%sM%dS" h m s)) @@ -280,8 +295,12 @@ (s/def ::version (s/with-gen (s/and string? - (partial re-matches xAPIVersionRegEx)) - #(sgen/return "2.0.0"))) + #(re-matches + (case *xapi-version* + "1.0.3" xAPIVersionRegEx + "2.0.0" xAPIVersionRegEx200) + %)) + #(sgen/return *xapi-version*))) (s/def ::sha2 (s/with-gen @@ -1015,31 +1034,62 @@ (s/every ::context-group :into [])) +;; multispec for dynamic params +(defmulti context-version (fn [_] *xapi-version*)) + +(defmethod context-version "1.0.3" [_] + (conform-ns + "context" + (s/and + (s/keys :opt [:context/registration + :context/instructor + :context/team + :context/contextActivities + :context/revision + :context/platform + :context/language + :context/statement + :context/extensions]) + (restrict-keys :context/registration + :context/instructor + :context/team + :context/contextActivities + :context/revision + :context/platform + :context/language + :context/statement + :context/extensions)))) + +(defmethod context-version "2.0.0" [_] + (conform-ns + "context" + (s/and + (s/keys :opt [:context/registration + :context/instructor + :context/team + :context/contextActivities + :context/revision + :context/platform + :context/language + :context/statement + :context/extensions + :context/contextAgents + :context/contextGroups]) + (restrict-keys :context/registration + :context/instructor + :context/team + :context/contextActivities + :context/revision + :context/platform + :context/language + :context/statement + :context/extensions + :context/contextAgents + :context/contextGroups)))) + (s/def ::context - (conform-ns "context" - (s/and - (s/keys :opt [:context/registration - :context/instructor - :context/team - :context/contextActivities - :context/revision - :context/platform - :context/language - :context/statement - :context/extensions - :context/contextAgents - :context/contextGroups]) - (restrict-keys :context/registration - :context/instructor - :context/team - :context/contextActivities - :context/revision - :context/platform - :context/language - :context/statement - :context/extensions - :context/contextAgents - :context/contextGroups)))) + (s/multi-spec context-version (fn [gen-val _] + gen-val) )) ;; Attachments diff --git a/src/xapi_schema/spec/regex.cljc b/src/xapi_schema/spec/regex.cljc index fc57e1a..dfd39a5 100644 --- a/src/xapi_schema/spec/regex.cljc +++ b/src/xapi_schema/spec/regex.cljc @@ -117,7 +117,7 @@ ;; Time time (str "(?:" hour ":" min ":" sec sec-frac "?" ")") date (str "(?:" year "-" month "-" day ")")] - (str date "[T\\s]" time))) + (str date "T" time))) (def TimestampRegEx ; RFC 3339 (let [;; Time @@ -144,8 +144,55 @@ dur-week)] (re-pattern (str "^P(?:" duration ")|P(?:" (base-timestamp) ")$")))) +(defn- base-timestamp-200 [] + (let [;; Date + year "(\\d{4})" + month "(0[1-9]|1[0-2])" + day "(0[1-9]|[12]\\d|3[01])" ; ignore month/leap year constraints + ;; Time + hour "([01]\\d|2[0-3])" + min "([0-5]\\d)" + sec "([0-5]\\d|60)" ; leap seconds + sec-frac "(\\.\\d+)" + ;; Time + time (str "(?:" hour ":" min ":" sec sec-frac "?" ")") + date (str "(?:" year "-" month "-" day ")")] + (str date "[T\\s]" time))) + +(def TimestampRegEx200 ; RFC 3339 + (let [;; Time + hour "(?:[01]\\d|2[0-3])" + min "(?:[0-5]\\d)" + ;; Offset + lookahead "(?!-00:00)" + num-offset (str "(?:[+-]" hour ":" min ")") + time-offset (str "(Z|" lookahead num-offset ")")] + (re-pattern (str "^" (base-timestamp-200) time-offset "$")))) + +(def DurationRegEx200 ; ISO 8601 Durations + (let [dy "(?:\\d+Y|\\d+\\.\\d+Y$)" + dm "(?:\\d+M|\\d+\\.\\d+M$)" + dw "(?:\\d+W|\\d+\\.\\d+W$)" + dd "(?:\\d+D|\\d+\\.\\d+D$)" + dh "(?:\\d+H|\\d+\\.\\d+H$)" + ds "(?:\\d+S|\\d+\\.\\d+S$)" + dur-date (str "(?:" dd "|" dm dd "?" "|" dy dm "?" dd "?" ")") + dur-time (str "(?:" ds "|" dm ds "?" "|" dh dm "?" ds "?" ")") + dur-week (str "(?:" dw ")") + duration (str "(?:" dur-date "(?:T" dur-time ")?" ")" "|" + "(?:T" dur-time ")" "|" + dur-week)] + (re-pattern (str "^P(?:" duration ")|P(?:" (base-timestamp-200) ")$")))) + + ;; Based on http://www.regexr.com/39s32 (def xAPIVersionRegEx + (let [suf-part "[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*" + suffix (str "(\\.[0-9]+(?:-" suf-part ")?(?:\\+" suf-part ")?)?") + ver-str (str "^1\\.0" suffix "$")] + (re-pattern ver-str))) + +(def xAPIVersionRegEx200 (let [suf-part "[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*" suffix (str "(\\.[0-9]+(?:-" suf-part ")?(?:\\+" suf-part ")?)?") ver-str (str "^(1\\.0" suffix ")|(2\\.0\\.0)$")] diff --git a/test/xapi_schema/spec/regex_test.cljc b/test/xapi_schema/spec/regex_test.cljc index c2356e4..d83b74c 100644 --- a/test/xapi_schema/spec/regex_test.cljc +++ b/test/xapi_schema/spec/regex_test.cljc @@ -9,7 +9,9 @@ MailToIRIRegEx UuidRegEx TimestampRegEx + TimestampRegEx200 xAPIVersionRegEx + xAPIVersionRegEx200 DurationRegEx Base64RegEx Sha1RegEx @@ -147,7 +149,7 @@ ;; negative offset (is (not (re-matches TimestampRegEx "2008-09-15T15:53:00.601-00:00")))) (testing "matches valid but terrible stamps in rfc3339 OUTSIDE of 8601" - (is (re-matches TimestampRegEx "2015-05-13 15:16:00Z")))) + (is (re-matches TimestampRegEx200 "2015-05-13 15:16:00Z")))) (deftest xapi-version-regex-test (testing "matches xAPI 1.0.X versions" @@ -157,8 +159,8 @@ (re-matches xAPIVersionRegEx "1.0.32-abc.def+ghi.jkl"))) (is (not (re-matches xAPIVersionRegEx "0.9.5")))) (testing "matches xAPI 2.0.0 version only" - (is (and (re-matches xAPIVersionRegEx "2.0.0") - (not (re-matches xAPIVersionRegEx "2.0.2")))))) + (is (and (re-matches xAPIVersionRegEx200 "2.0.0") + (not (re-matches xAPIVersionRegEx200 "2.0.2")))))) (deftest duration-regex-test (testing "matches ISO durations" diff --git a/test/xapi_schema/spec_test.cljc b/test/xapi_schema/spec_test.cljc index b7d6e12..467534b 100644 --- a/test/xapi_schema/spec_test.cljc +++ b/test/xapi_schema/spec_test.cljc @@ -389,7 +389,37 @@ :bad {"team" {"mbox" "mailto:a@b.com"}} {"team" {"mbox" "mailto:a@b.com" - "objectType" "Agent"}})))) + "objectType" "Agent"}}))) + (testing "xAPI 2.0.0" + (binding [xs/*xapi-version* "2.0.0"] + (testing "contextAgents" + (should-satisfy+ + ::xs/context + {"contextAgents" + [{:objectType "contextAgent" + :agent {"mbox" "mailto:a@b.com" + "objectType" "Agent"}}]} + :bad + {"contextAgents" [{"mbox" "mailto:a@b.com" + "objectType" "Agent"}]} + {"contextAgents" + [{:objectType "contextGroup" + :group {"mbox" "mailto:a@b.com" + "objectType" "Group"}}]})) + (testing "contextGroups" + (should-satisfy+ + ::xs/context + {"contextGroups" + [{:objectType "contextGroup" + :group {"mbox" "mailto:a@b.com" + "objectType" "Group"}}]} + :bad + {"contextGroups" [{"mbox" "mailto:a@b.com" + "objectType" "Group"}]} + {"contextGroups" + [{:objectType "contextAgent" + :agent {"mbox" "mailto:a@b.com" + "objectType" "Agent"}}]}))))) (deftest attachment-test (testing