├── .lein-spell
├── src
└── clj_json_ld
│ ├── compact.clj
│ ├── flatten.clj
│ ├── json_ld_error.clj
│ ├── util
│ ├── format.clj
│ └── zipmap_filter.clj
│ ├── json_ld.clj
│ ├── core.clj
│ ├── value.clj
│ ├── iri.clj
│ ├── context.clj
│ ├── term_definition.clj
│ └── expand.clj
├── .travis.yml
├── .gitignore
├── test
└── clj_json_ld
│ ├── unit
│ ├── expansion.clj
│ ├── iri.clj
│ ├── value.clj
│ └── context.clj
│ ├── spec
│ ├── expansion.clj
│ ├── compaction.clj
│ └── flattening.clj
│ └── util
│ └── spec_test_suite.clj
├── doc
└── API
│ ├── clj-json-ld.json-ld.html
│ ├── index.html
│ ├── js
│ └── page_effects.js
│ ├── clj-json-ld.core.html
│ └── css
│ └── default.css
├── project.clj
├── LICENSE.txt
└── README.md
/.lein-spell:
--------------------------------------------------------------------------------
1 | acknowledgements
2 | deserialize
3 | interoperable
4 | interpretable
5 | midje
6 | pyld
7 | resellers
8 | über
--------------------------------------------------------------------------------
/src/clj_json_ld/compact.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.compact
2 | "
3 | Implement the Compaction operation as defined in the
4 | [Compaction Algorithm section](http://www.w3.org/TR/json-ld-api/#compaction-algorithm).
5 | ")
6 |
7 | (defn compact-it [input context options]
8 | "{}")
--------------------------------------------------------------------------------
/src/clj_json_ld/flatten.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.flatten
2 | "
3 | Implement the Flattening operation as defined in the
4 | [Flattening Algorithm section](http://www.w3.org/TR/json-ld-api/#flattening-algorithm).
5 | ")
6 |
7 | (defn flatten-it [input context options]
8 | "[]")
--------------------------------------------------------------------------------
/src/clj_json_ld/json_ld_error.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.json-ld-error
2 | "Aborting errors detected during processing.")
3 |
4 | ;; TODO Use a real Java exception here, not ex-info
5 | (defn json-ld-error [code message]
6 | (throw (ex-info "JSONLDError" {:code code :message message})))
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: clojure
2 | lein: lein2
3 | script:
4 | - git clone https://github.com/json-ld/tests json-ld-tests
5 | - lein2 midje
6 | - lein2 eastwood
7 | branches:
8 | only:
9 | - master
10 | - dev
11 | jdk:
12 | - oraclejdk7
13 | notifications:
14 | hipchat:
15 | rooms:
16 | - ec1ead2b384476ea5bcb232b5bb9b6@Snooty Monkey
--------------------------------------------------------------------------------
/src/clj_json_ld/util/format.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.util.format
2 | (:require [cheshire.core :refer (parse-string generate-string)]))
3 |
4 | (defn ingest-input
5 | ""
6 | [input options]
7 | ;; Parse the JSON if need be
8 | (if (map? input) input (parse-string input)))
9 |
10 | (defn format-output
11 | ""
12 | [json options]
13 | (generate-string json))
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #####=== Clojure ===#####
2 |
3 | pom.xml
4 | pom.xml.asc
5 | /lib/
6 | /classes/
7 | /target/
8 | /checkouts/
9 | .lein-deps-sum
10 | .lein-repl-history
11 | .lein-plugins/
12 | .lein-failures
13 | .nrepl-port
14 |
15 |
16 | #####=== Java ===#####
17 |
18 | *.class
19 |
20 | # Mobile Tools for Java (J2ME)
21 | .mtj.tmp/
22 |
23 | # Package Files #
24 | *.jar
25 | *.war
26 | *.ear
27 |
28 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
29 | hs_err_pid*
30 |
31 |
32 | #####=== OSX ===#####
33 |
34 | .DS_Store
35 | .AppleDouble
36 | .LSOverride
37 |
38 | # Icon must end with two \r
39 | Icon
40 |
41 |
42 | # Thumbnails
43 | ._*
44 |
45 | # Files that might appear on external disk
46 | .Spotlight-V100
47 | .Trashes
48 |
49 | # Directories potentially created on remote AFP share
50 | .AppleDB
51 | .AppleDesktop
52 | Network Trash Folder
53 | Temporary Items
54 | .apdisk
--------------------------------------------------------------------------------
/test/clj_json_ld/unit/expansion.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.unit.expansion
2 | "Additional unit tests for expansion beyond what's provided in the spec."
3 | (:require [midje.sweet :refer :all]
4 | [cheshire.core :refer (parse-string)]
5 | [clj-json-ld.core :as json-ld]))
6 |
7 | (def simple-list-jsonld {"http://example.org/list2" {"@list" [{"@value" nil}]}})
8 | (def simple-list-jsonld2 {"@context" {"mylist2" {"@id" "http://example.com/mylist2" "@container" "@list"}}
9 | "mylist2" "one item"})
10 | (def simple-list-jsonld3 {"@context" {"mylist1" {"@id" "http://example.com/mylist1" "@container" "@list"}}
11 | "mylist1" {"@list" []}})
12 |
13 | (facts "list expansion works per the spec"
14 | (parse-string (json-ld/expand simple-list-jsonld)) => [{"http://example.org/list2" [{"@list" []}]}]
15 | (parse-string (json-ld/expand simple-list-jsonld2)) => [{"http://example.com/mylist2" [{"@list" [{"@value" "one item"}]}]}]
16 | (parse-string (json-ld/expand simple-list-jsonld3)) => [{"http://example.com/mylist1" [{"@list" []}]}])
--------------------------------------------------------------------------------
/test/clj_json_ld/spec/expansion.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.spec.expansion
2 | (:require [clojure.pprint :refer (pprint)]
3 | [midje.sweet :refer :all]
4 | [cheshire.core :refer (parse-string)]
5 | [clj-json-ld.util.spec-test-suite :refer :all]
6 | [clj-json-ld.core :as json-ld]))
7 |
8 | (def manifest "expand-manifest.jsonld")
9 |
10 | (facts "Expansion Evaluation Tests"
11 |
12 | (doseq [test-case (take 4 (tests-from-manifest manifest))]
13 |
14 | (print-test "Expansion" test-case)
15 |
16 | ;; Possible variations:
17 | ;; input - JSON, map, remote file
18 | ;; output - JSON, map
19 |
20 | ;; Combinatorial: 3 x 2 = 6 total cases
21 |
22 | ;; 1,"JSON","JSON"
23 | ;; 2,"JSON", "map"
24 | ;; 3,"map", "JSON"
25 | ;; 4,"map","map"
26 | ;; 5,"remote","JSON"
27 | ;; 6,"remote","map"
28 |
29 | ;; 1,"JSON","JSON"
30 | (let [result (parse-string (json-ld/expand (:input test-case)))]
31 | (println "\nActual:")
32 | (pprint result)
33 | result =>(parse-string (:expect test-case)))
34 |
35 | ;; 2,"JSON", "map"
36 |
37 | ;; 3,"map", "JSON"
38 | ; (parse-string (json-ld/expand (parse-string (:input test-case)))) =>
39 | ; (parse-string (:expect test-case))
40 |
41 | ;; 4,"map","map"
42 |
43 | ;; 5,"remote","JSON"
44 |
45 | ;; 6,"remote","map"
46 |
47 | )
48 | )
--------------------------------------------------------------------------------
/test/clj_json_ld/util/spec_test_suite.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.util.spec-test-suite
2 | (:require [clojure.java.io :as io]
3 | [clojure.walk :refer (keywordize-keys)]
4 | [cheshire.core :refer (parse-string, parse-stream)]))
5 |
6 | ;; JSON-LD tests git repo may be in this dir or the parent dir and may be called json-ld-tests or tests
7 | (def possible-spec-dirs ["../json-ld-tests/" "../tests/" "./json-ld-tests/" "./tests/"])
8 | (def tests-location (first (filter #(.isDirectory (io/file %)) possible-spec-dirs)))
9 |
10 | (defn- load-manifest
11 | "Given a manifest file name, load it from the tests dir"
12 | [manifest-file]
13 | (parse-stream (clojure.java.io/reader (str tests-location manifest-file))))
14 |
15 | (defn tests-from-manifest
16 | "Load the :sequence vector from the manifest file and replace
17 | the :input, :expect and :context values in each test case with the
18 | JSON string contents of the file they point to."
19 | [manifest-file]
20 | (->> (:sequence (keywordize-keys (load-manifest manifest-file)))
21 | (map #(assoc % :input (slurp (str tests-location (:input %)))))
22 | (map #(assoc % :expect (slurp (str tests-location (:expect %)))))
23 | (map #(assoc % :context (if (:context %) (slurp (str tests-location (:context %))) nil)))))
24 |
25 | (defn print-test
26 | "Print output explaining the test case."
27 | [test-type test-case]
28 | (println (str "\n" test-type) "Test:" (get test-case (keyword "@id")) (:name test-case))
29 | (println (str "Purpose: " (:purpose test-case)))
30 | (println "\nInput:\n" (:input test-case))
31 | (println "Expected:\n" (:expect test-case)))
--------------------------------------------------------------------------------
/test/clj_json_ld/spec/compaction.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.spec.compaction
2 | (:require [midje.sweet :refer :all]
3 | [cheshire.core :refer (parse-string)]
4 | [clj-json-ld.util.spec-test-suite :refer :all]
5 | [clj-json-ld.core :as json-ld]))
6 |
7 | (def manifest "compact-manifest.jsonld")
8 |
9 | (facts "Compact Evaluation Tests"
10 |
11 | (doseq [test-case (take 1 (tests-from-manifest manifest))]
12 |
13 | (print-test "Compaction" test-case)
14 |
15 | ;; Possible variations:
16 | ;; input - JSON, map, remote file
17 | ;; context - JSON, map, remote file
18 | ;; output - JSON, map
19 |
20 | ;; Combinatorial: 3 x 3 x 2 = 18 total cases
21 |
22 | ;; Optimal pairwise test set of 9:
23 | ;; 1,"JSON","JSON","JSON"
24 | ;; 2,"map","JSON","map"
25 | ;; 3,"JSON","map","map"
26 | ;; 4,"map","map","JSON"
27 | ;; 5,"remote","JSON","JSON"
28 | ;; 6,"JSON","remote","JSON"
29 | ;; 7,"map","remote","map"
30 | ;; 8,"remote","map","map"
31 | ;; 9,"remote","remote","JSON"
32 |
33 | ;; 1,"JSON","JSON","JSON"
34 | (parse-string (json-ld/compact (:input test-case) (:context test-case))) =>
35 | (parse-string (:expect test-case))
36 |
37 | ;; 2,"map","JSON","map"
38 |
39 | ;; 3,"JSON","map","map"
40 |
41 | ;; 4,"map","map","JSON"
42 | (parse-string (json-ld/compact (parse-string (:input test-case))
43 | (parse-string (:context test-case)))) => (parse-string (:expect test-case))
44 |
45 | ;; 5,"remote","JSON","JSON"
46 | ;; 6,"JSON","remote","JSON"
47 | ;; 7,"map","remote","map"
48 | ;; 8,"remote","map","map"
49 | ;; 9,"remote","remote","JSON"
50 |
51 | )
52 | )
--------------------------------------------------------------------------------
/test/clj_json_ld/spec/flattening.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.spec.flattening
2 | (:refer-clojure :exclude [flatten])
3 | (:require [midje.sweet :refer :all]
4 | [cheshire.core :refer (parse-string)]
5 | [clj-json-ld.util.spec-test-suite :refer :all]
6 | [clj-json-ld.core :as json-ld]))
7 |
8 | (def manifest "flatten-manifest.jsonld")
9 |
10 | (facts "Flatten Evaluation Tests"
11 |
12 | (doseq [test-case (take 1 (tests-from-manifest manifest))]
13 |
14 | (print-test "Flatten" test-case)
15 |
16 | ;; Possible variation:
17 | ;; input - JSON, map, remote file
18 | ;; context - JSON, map, remote file
19 | ;; output - JSON, map
20 |
21 | ;; Combinatorial: 3 x 3 x 2 = 18 total cases
22 |
23 | ;; Optimal pairwise test set of 9:
24 | ;; 1,"JSON","JSON","JSON"
25 | ;; 2,"map","JSON","map"
26 | ;; 3,"JSON","map","map"
27 | ;; 4,"map","map","JSON"
28 | ;; 5,"remote","JSON","JSON"
29 | ;; 6,"JSON","remote","JSON"
30 | ;; 7,"map","remote","map"
31 | ;; 8,"remote","map","map"
32 | ;; 9,"remote","remote","JSON"
33 |
34 | ;; 1,"JSON","JSON","JSON"
35 | (parse-string (json-ld/flatten (:input test-case) (:context test-case))) =>
36 | (parse-string (:expect test-case))
37 |
38 | ;; 2,"map","JSON","map"
39 |
40 | ;; 3,"JSON","map","map"
41 |
42 | ;; 4,"map","map","JSON"
43 | (parse-string (json-ld/flatten (parse-string (:input test-case))
44 | (parse-string (:context test-case)))) => (parse-string (:expect test-case))
45 |
46 | ;; 5,"remote","JSON","JSON"
47 |
48 | ;; 6,"JSON","remote","JSON"
49 |
50 | ;; 7,"map","remote","map"
51 |
52 | ;; 8,"remote","map","map"
53 |
54 | ;; 9,"remote","remote","JSON"
55 |
56 | )
57 | )
--------------------------------------------------------------------------------
/src/clj_json_ld/json_ld.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.json-ld
2 | "
3 | Define the JSON-LD keywords as specified in the
4 | [Syntax Tokens and Keywords section](http://www.w3.org/TR/2014/REC-json-ld-20140116/#syntax-tokens-and-keywords).
5 | ")
6 |
7 | (def keywords #{
8 | "@context" ; Used to define the short-hand names that are used throughout a JSON-LD document. These short-hand names are called terms and help developers to express specific identifiers in a compact manner. The @context keyword is described in detail in section 5.1 The Context.
9 | "@id" ; Used to uniquely identify things that are being described in the document with IRIs or blank node identifiers. This keyword is described in section 5.3 Node Identifiers.
10 | "@value" ; Used to specify the data that is associated with a particular property in the graph. This keyword is described in section 6.9 String Internationalization and section 6.4 Typed Values.
11 | "@language" ; Used to specify the language for a particular string value or the default language of a JSON-LD document. This keyword is described in section 6.9 String Internationalization.
12 | "@type" ;Used to set the data type of a node or typed value. This keyword is described in section 6.4 Typed Values.
13 | "@container" ; Used to set the default container type for a term. This keyword is described in section 6.11 Sets and Lists.
14 | "@list" ; Used to express an ordered set of data. This keyword is described in section 6.11 Sets and Lists.
15 | "@set" ; Used to express an unordered set of data and to ensure that values are always represented as arrays. This keyword is described in section 6.11 Sets and Lists.
16 | "@reverse" ; Used to express reverse properties. This keyword is described in section 6.12 Reverse Properties.
17 | "@index" ; Used to specify that a container is used to index information and that processing should continue deeper into a JSON data structure. This keyword is described in section 6.16 Data Indexing.
18 | "@base" ;Used to set the base IRI against which relative IRIs are resolved. This keyword is described in section 6.1 Base IRI.
19 | "@vocab" ; Used to expand properties and values in @type with a common prefix IRI. This keyword is described in section 6.2 Default Vocabulary.
20 | "@graph" ; Used to express a graph. This keyword is described in section 6.13 Named Graphs.
21 | })
22 |
23 | (defn json-ld-keyword?
24 | "Returns `true` if the value is a JSON-LD keyword, otherwise `false`."
25 | [value]
26 | (if (keywords value) true false))
--------------------------------------------------------------------------------
/doc/API/clj-json-ld.json-ld.html:
--------------------------------------------------------------------------------
1 |
2 |
clj-json-ld.json-ld documentation clj-json-ld.json-ld json-ld-keyword? (json-ld-keyword? value)
Returns true if the value is a JSON-LD keyword, otherwise false.
--------------------------------------------------------------------------------
/doc/API/index.html:
--------------------------------------------------------------------------------
1 |
2 | Clj-json-ld 0.1.0-SNAPSHOT API documentation Clj-json-ld 0.1.0-SNAPSHOT Public variables and functions:
Public variables and functions:
--------------------------------------------------------------------------------
/src/clj_json_ld/util/zipmap_filter.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.util.zipmap-filter
2 | (:require [defun :refer (defun)]))
3 |
4 | ; Falklandsophile begat FCMS begat clj-json-ld begat zipmap-filter
5 |
6 | ; New library project
7 | ; New GH repo
8 | ; Move the namespace
9 | ; Create the tests
10 | ; Add direct filter-map functions
11 | ; Generate API docs - maybe use https://github.com/gdeer81/marginalia like http://atroche.github.io/clj-sockets/
12 | ; Create the tests
13 | ; Add core.typed support
14 | ; Readme
15 | ; CI on travis
16 | ; submit to clojars
17 | ; add to SM.com
18 | ; submit to clojure mailing list
19 |
20 | (defun zipmap-filter
21 | "Returns a map with the keys mapped to the corresponding values for the keys and values that satisfy the predicate."
22 | ([pred :guard #(not (associative? %)) ks vs] (recur {} [pred (seq ks)] [pred (seq vs)]))
23 | ([key-pred value-pred ks vs] (recur {} [key-pred (seq ks)] [value-pred (seq vs)]))
24 | ([map ks-tuple :guard #(empty? (last %)) vs-tuple] map)
25 | ([map ks-tuple vs-tuple :guard #(empty? (last %))] map)
26 | ([map ks-tuple :guard #(not ((first %) (first (last %)))) vs-tuple] (recur map [(first ks-tuple) (next (last ks-tuple))] [(first vs-tuple) (next (last vs-tuple))]))
27 | ([map ks-tuple vs-tuple :guard #(not ((first %) (first (last %))))] (recur map [(first ks-tuple) (next (last ks-tuple))] [(first vs-tuple) (next (last vs-tuple))]))
28 | ([map ks-tuple vs-tuple] (recur (assoc map (first (last ks-tuple)) (first (last vs-tuple))) [(first ks-tuple) (next (last ks-tuple))] [(first vs-tuple) (next (last vs-tuple))])))
29 |
30 | (defun zipmap-filter-keys
31 | "Returns a map with the keys mapped to the corresponding values for the keys that satisfy the predicate."
32 | ([pred :guard #(not (associative? %)) ks vs] (recur {} [pred (seq ks)] (seq vs)))
33 | ([map ks-tuple :guard #(empty? (last %)) vs] map)
34 | ([map ks vs :guard empty?] map)
35 | ([map ks-tuple :guard #((first %) (first (last %))) vs] (recur (assoc map (first (last ks-tuple)) (first vs)) [(first ks-tuple) (next (last ks-tuple))] (next vs)))
36 | ([map ks-tuple vs] (recur map [(first ks-tuple) (next (last ks-tuple))] (next vs))))
37 |
38 | (defun zipmap-filter-values
39 | "Returns a map with the keys mapped to the corresponding values for the values that satisfy the predicate."
40 | ([pred :guard #(not (associative? %)) ks vs] (recur {} (seq ks) [pred (seq vs)]))
41 | ([map ks :guard empty? vs-tuple] map)
42 | ([map ks vs-tuple :guard #(empty? (last %))] map)
43 | ([map ks vs-tuple :guard #((first %) (first (last %)))] (recur (assoc map (first ks) (first (last vs-tuple))) (next ks) [(first vs-tuple) (next (last vs-tuple))]))
44 | ([map ks vs-tuple] (recur map (next ks) [(first vs-tuple) (next (last vs-tuple))])))
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject clj-json-ld "0.1.0-SNAPSHOT"
2 | :description "JSON-LD for Clojure"
3 | :url "https://github.com/SnootyMonkey/clj-json-ld"
4 | :license {
5 | :name "Mozilla Public License v2.0"
6 | :url "http://www.mozilla.org/MPL/2.0/"
7 | }
8 | :support {
9 | :name "Sean Johnson"
10 | :email "sean@snootymonkey.com"
11 | }
12 |
13 | :min-lein-version "2.5.0" ; highest version supported by Travis-CI as of 10/28/2014
14 |
15 | :dependencies [
16 | [org.clojure/clojure "1.7.0"] ; Lisp on the JVM http://clojure.org/documentation
17 | [org.clojure/core.match "0.3.0-alpha4"] ; Erlang-esque pattern matching https://github.com/clojure/core.match
18 | [defun "0.2.0-RC"] ; Erlang-esque pattern matching for Clojure functions https://github.com/killme2008/defun
19 | [cheshire "5.5.0"] ; JSON de/encoding https://github.com/dakrone/cheshire
20 | [clojurewerkz/urly "2.0.0-alpha5"] ; URI and URL parsing library https://github.com/michaelklishin/urly
21 | ]
22 |
23 | :profiles {
24 | :dev {
25 |
26 | :dependencies [
27 | [midje "1.7.0"] ; Example-based testing https://github.com/marick/Midje
28 | ]
29 |
30 | :plugins [
31 | [codox "0.8.13"] ; Generate Clojure API docs https://github.com/weavejester/codox
32 | [lein-midje "3.2-RC4"] ; Example-based testing https://github.com/marick/lein-midje
33 | [lein-bikeshed "0.2.0"] ; Check for code smells https://github.com/dakrone/lein-bikeshed
34 | [lein-kibit "0.1.2"] ; Static code search for non-idiomatic code https://github.com/jonase/kibit
35 | [jonase/eastwood "0.2.1"] ; Clojure linter https://github.com/jonase/eastwood
36 | [lein-checkall "0.1.1"] ; Runs bikeshed, kibit and eastwood https://github.com/itang/lein-checkall
37 | [lein-ancient "0.6.7"] ; Check for outdated dependencies https://github.com/xsc/lein-ancient
38 | [lein-spell "0.1.0"] ; Catch spelling mistakes in docs and docstrings https://github.com/cldwalker/lein-spell
39 | ]
40 |
41 | :codox {
42 | :sources ["src/"]
43 | :include [clj-json-ld.core clj-json-ld.json-ld]
44 | :output-dir "doc/API"
45 | :src-dir-uri "http://github.com/SnootyMonkey/clj-json-ld/blob/master/"
46 | :src-linenum-anchor-prefix "L" ; for Github
47 | :defaults {:doc/format :markdown}
48 | }
49 | }
50 | }
51 |
52 | :aliases {
53 | "build" ["do" "clean," "deps," "compile"] ; clean and build code
54 | "midje" ["with-profile" "dev" "midje"] ; run all tests
55 | "spell!" ["spell" "-n"] ; check spelling in docs and docstrings
56 | "bikeshed!" ["bikeshed" "-v" "-m" "120"] ; code check with max line length warning of 120 characters
57 | "ancient" ["with-profile" "dev" "do" "ancient" ":allow-qualified," "ancient" ":plugins" ":allow-qualified"] ; check for out of date dependencies
58 | }
59 |
60 | ;; ----- Code check configuration -----
61 |
62 | :eastwood {
63 | ;; Enable some linters that are disabled by default
64 | :add-linters [:unused-namespaces :unused-private-vars]
65 | :exclude-linters [:suspicious-expression]
66 | ;; More extensive lintering that will have a few false positives
67 | ;; :add-linters [:unused-namespaces :unused-private-vars :unused-locals :unused-fn-args]
68 |
69 | ;; Exclude testing namespaces
70 | :tests-paths ["test"]
71 | :exclude-namespaces [:test-paths]
72 | }
73 | )
--------------------------------------------------------------------------------
/doc/API/js/page_effects.js:
--------------------------------------------------------------------------------
1 | function visibleInParent(element) {
2 | var position = $(element).position().top
3 | return position > -50 && position < ($(element).offsetParent().height() - 50)
4 | }
5 |
6 | function hasFragment(link, fragment) {
7 | return $(link).attr("href").indexOf("#" + fragment) != -1
8 | }
9 |
10 | function findLinkByFragment(elements, fragment) {
11 | return $(elements).filter(function(i, e) { return hasFragment(e, fragment)}).first()
12 | }
13 |
14 | function scrollToCurrentVarLink(elements) {
15 | var elements = $(elements);
16 | var parent = elements.offsetParent();
17 |
18 | if (elements.length == 0) return;
19 |
20 | var top = elements.first().position().top;
21 | var bottom = elements.last().position().top + elements.last().height();
22 |
23 | if (top >= 0 && bottom <= parent.height()) return;
24 |
25 | if (top < 0) {
26 | parent.scrollTop(parent.scrollTop() + top);
27 | }
28 | else if (bottom > parent.height()) {
29 | parent.scrollTop(parent.scrollTop() + bottom - parent.height());
30 | }
31 | }
32 |
33 | function setCurrentVarLink() {
34 | $('#vars a').parent().removeClass('current')
35 | $('.anchor').
36 | filter(function(index) { return visibleInParent(this) }).
37 | each(function(index, element) {
38 | findLinkByFragment("#vars a", element.id).
39 | parent().
40 | addClass('current')
41 | });
42 | scrollToCurrentVarLink('#vars .current');
43 | }
44 |
45 | var hasStorage = (function() { try { return localStorage.getItem } catch(e) {} }())
46 |
47 | function scrollPositionId(element) {
48 | var directory = window.location.href.replace(/[^\/]+\.html$/, '')
49 | return 'scroll::' + $(element).attr('id') + '::' + directory
50 | }
51 |
52 | function storeScrollPosition(element) {
53 | if (!hasStorage) return;
54 | localStorage.setItem(scrollPositionId(element) + "::x", $(element).scrollLeft())
55 | localStorage.setItem(scrollPositionId(element) + "::y", $(element).scrollTop())
56 | }
57 |
58 | function recallScrollPosition(element) {
59 | if (!hasStorage) return;
60 | $(element).scrollLeft(localStorage.getItem(scrollPositionId(element) + "::x"))
61 | $(element).scrollTop(localStorage.getItem(scrollPositionId(element) + "::y"))
62 | }
63 |
64 | function persistScrollPosition(element) {
65 | recallScrollPosition(element)
66 | $(element).scroll(function() { storeScrollPosition(element) })
67 | }
68 |
69 | function sidebarContentWidth(element) {
70 | var widths = $(element).find('.inner').map(function() { return $(this).innerWidth() })
71 | return Math.max.apply(Math, widths)
72 | }
73 |
74 | function resizeSidebars() {
75 | var nsWidth = sidebarContentWidth('#namespaces') + 30
76 | var varWidth = 0
77 |
78 | if ($('#vars').length != 0) {
79 | varWidth = sidebarContentWidth('#vars') + 30
80 | }
81 |
82 | // snap to grid
83 | var snap = 30;
84 | nsWidth = Math.ceil(nsWidth / snap) * snap;
85 | varWidth = Math.ceil(varWidth / snap) * snap;
86 |
87 | $('#namespaces').css('width', nsWidth)
88 | $('#vars').css('width', varWidth)
89 | $('#vars, .namespace-index').css('left', nsWidth + 1)
90 | $('.namespace-docs').css('left', nsWidth + varWidth + 2)
91 | }
92 |
93 | $(window).ready(resizeSidebars)
94 | $(window).ready(setCurrentVarLink)
95 | $(window).ready(function() { persistScrollPosition('#namespaces')})
96 | $(window).ready(function() {
97 | $('#content').scroll(setCurrentVarLink)
98 | $(window).resize(setCurrentVarLink)
99 | })
100 |
--------------------------------------------------------------------------------
/src/clj_json_ld/core.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.core
2 | "
3 | Implements the [Expansion](http://www.w3.org/TR/json-ld-api/#expansion),
4 | [Compaction](http://www.w3.org/TR/json-ld-api/#compaction),
5 | [Flattening](http://www.w3.org/TR/json-ld-api/#rdf-serialization-deserialization) and
6 | [RDF Serialization/Deserialization](http://www.w3.org/TR/json-ld-api/#rdf-serialization-deserialization)
7 | operations as defined in the
8 | [JSON-LD 1.0 Processing Algorithms and API specification](http://www.w3.org/TR/json-ld-api/)"
9 | (:refer-clojure :exclude [flatten])
10 | (:require [clj-json-ld.compact :refer (compact-it)]
11 | [clj-json-ld.expand :refer (expand-it)]
12 | [clj-json-ld.flatten :refer (flatten-it)]))
13 |
14 | (defn expand
15 | "
16 | [Expands](http://www.w3.org/TR/json-ld-api/#expansion) the given JSON-LD input according
17 | to the steps in the
18 | [JSON-LD Expansion Algorithm](http://www.w3.org/TR/json-ld-api/#expansion-algorithm).
19 |
20 | **input** - a JSON-LD document as a Unicode text string, or a
21 | [Map](http://clojure.org/data_structures#Data%20Structures-Maps%20%28IPersistentMap%29)
22 | representing a parsed JSON-LD document, or a string URL to a remote JSON-LD document.
23 |
24 | **options** *(optional)* -
25 |
26 | Returns an expanded JSON-LD document as a Unicode text string, or a
27 | [Map](http://clojure.org/data_structures#Data%20Structures-Maps%20%28IPersistentMap%29)
28 | representing the expanded JSON-LD document.
29 | "
30 | ([input] (expand input {}))
31 | ([input options]
32 | (expand-it input options)))
33 |
34 | (defn compact
35 | "
36 | [Compacts](http://www.w3.org/TR/json-ld-api/#compaction) the given JSON-LD input according
37 | to the steps in the
38 | [JSON-LD Compaction Algorithm](http://www.w3.org/TR/json-ld-api/#compaction-algorithm).
39 |
40 | Compaction is performed using the provided context. If no context is provided, the
41 | input document is compacted using the top-level context of the document.
42 |
43 | **input** - a JSON-LD document as a Unicode text string, or a
44 | [Map](http://clojure.org/data_structures#Data%20Structures-Maps%20%28IPersistentMap%29)
45 | representing a parsed JSON-LD document, or a string URL to a remote JSON-LD document.
46 |
47 | **context** *(optional)* - a JSON-LD context document as a Unicode text string, or a
48 | [Map](http://clojure.org/data_structures#Data%20Structures-Maps%20%28IPersistentMap%29)
49 | representing a parsed JSON-LD context document, or a string URL to a remote JSON-LD
50 | context document. If you want to provide `options` but not a `context`, pass in `nil`
51 | for the `context`.
52 |
53 | **options** *(optional)* -
54 |
55 | Returns a compacted a JSON-LD document as a Unicode text string, or a
56 | [Map](http://clojure.org/data_structures#Data%20Structures-Maps%20%28IPersistentMap%29)
57 | representing the compacted JSON-LD document.
58 | "
59 | ([input] (compact input nil))
60 | ([input context] (compact input context {}))
61 | ([input context options]
62 | (compact-it input context options)))
63 |
64 | (defn flatten
65 | "
66 | [Flattens](http://www.w3.org/TR/json-ld-api/#flattening) and compacts the given JSON-LD
67 | input according to the steps in the
68 | [JSON-LD Flattening Algorithm](http://www.w3.org/TR/json-ld-api/#flattening-algorithm).
69 |
70 | **input** - a JSON-LD document as a Unicode text string, or a
71 | [Map](http://clojure.org/data_structures#Data%20Structures-Maps%20%28IPersistentMap%29)
72 | representing a parsed JSON-LD document, or a string URL to a remote JSON-LD document.
73 |
74 | **context** *(optional)* - a JSON-LD context document as a Unicode text string, or a
75 | [Map](http://clojure.org/data_structures#Data%20Structures-Maps%20%28IPersistentMap%29)
76 | representing a parsed JSON-LD context document, or a string URL to a remote JSON-LD
77 | context document. If you want to provide `options` but not a `context`, pass in `nil`
78 | for the `context`.
79 |
80 | **options** *(optional)* -
81 |
82 | Returns a flattened a JSON-LD document as a Unicode text string, or a
83 | [Map](http://clojure.org/data_structures#Data%20Structures-Maps%20%28IPersistentMap%29)
84 | representing the flattened JSON-LD document.
85 | "
86 | ([input] (flatten input nil))
87 | ([input context] (flatten input context {}))
88 | ([input context options]
89 | (flatten-it input context options)))
--------------------------------------------------------------------------------
/src/clj_json_ld/value.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.value
2 | "
3 | Implement value expansion as defined in the
4 | [Value Expansion section](http://www.w3.org/TR/json-ld-api/#value-expansion).
5 |
6 | Implement value compaction as defined in the
7 | [Value Compaction section](http://www.w3.org/TR/json-ld-api/#value-compaction).
8 | "
9 | (:require [defun :refer (defun-)]
10 | [clj-json-ld.iri :refer (expand-iri)]))
11 |
12 | (defn- property-type-mapping
13 | "If active property has a type mapping in active context, return it if it's not @id."
14 | [active-context active-property]
15 | (let [type-mapping (get-in active-context [active-property "@type"])]
16 | (if (= type-mapping "@id") nil type-mapping)))
17 |
18 | (defn- value-is-an-iri?
19 | "Check the active context for a type mapping for the active property, and return true
20 | if the type is an IRI, and false if it is not."
21 | [args]
22 | (= (get-in (:active-context args) [(:active-property args) "@type"]) "@id"))
23 |
24 | (defun- language-mapping
25 | ;; If a language mapping is associated with active property in active context, add
26 | ;; an @language to result and set its value to the language code associated with the
27 | ;; language mapping; unless the language mapping is set to null in which case no member
28 | ;; is added.
29 | ([args :guard #(get-in % [:active-context (get % :active-property) "@language"])]
30 | (assoc (:value-map args) "@language" (get-in args [:active-context (get args :active-property) "@language"])))
31 |
32 | ;; Otherwise, if the active context has a default language, add an @language
33 | ;; to result and set its value to the default language.
34 | ([args :guard #(get-in % [:active-context "@language"])]
35 | (assoc (:value-map args) "@language" (get-in args [:active-context "@language"])))
36 |
37 | ;; Otherwise, there is no language mapping for you! Good luck guessing the language sucker!
38 | ([args] (:value-map args)))
39 |
40 | (defun- expand-it
41 | ;; 1) If the active property has a type mapping in active context that is @id,
42 | ;; return a new JSON object containing a single key-value pair where the key is @id
43 | ;; and the value is the result of using the IRI Expansion algorithm, passing active
44 | ;; context, value, and true for document relative.
45 | ([args :guard #(value-is-an-iri? %)]
46 | {"@id" (expand-iri (:active-context args) (:value args) {:document-relative true})})
47 |
48 | ;; 2) If active property has a type mapping in active context that is @vocab, return a new JSON object containing a single key-value pair where the key is @id and the value is the result of using the IRI Expansion algorithm, passing active context, value, true for vocab, and true for document relative.
49 |
50 | ;; 3) Otherwise, initialize result to a JSON object with an @value member whose value is set to value.
51 | ([args] {"@value" (:value args)}))
52 |
53 | (defn expand-value
54 | "
55 | [Value Expansion](http://www.w3.org/TR/json-ld-api/#value-expansion)
56 |
57 | Some values in JSON-LD can be expressed in a compact form. These values are required to
58 | be expanded at times when processing JSON-LD documents.
59 |
60 | If active property has a type mapping in the active context set to @id or @vocab, a JSON
61 | object with a single member @id whose value is the result of using the IRI Expansion
62 | algorithm on value is returned.
63 |
64 | Otherwise, the result will be a JSON object containing an @value member whose value is the
65 | passed value. Additionally, an @type member will be included if there is a type mapping
66 | associated with the active property or an @language member if value is a string and there
67 | is language mapping associated with the active property.
68 |
69 | **active-context** - context map used to resolve terms
70 |
71 | **active-property** - the property whose value is being expanded
72 |
73 | **value** - value to be expanded
74 | "
75 | [active-context active-property value]
76 | ;; Pairtially expand the value and get its type mapping
77 | (let [partially-expanded-value (expand-it {:active-context active-context
78 | :active-property active-property
79 | :value value})
80 | type-mapping (property-type-mapping active-context active-property)]
81 |
82 | ;; 4) If active property has a type mapping in active context [and it's not @id],
83 | ;; add an @type member to result and set its value to the value associated with the
84 | ;; type mapping.
85 | (if type-mapping
86 | (assoc partially-expanded-value "@type" type-mapping)
87 |
88 | ;; 5) Otherwise, if there is an @value, and it's a string, add the (optional)
89 | ;; language mapping
90 | (let [value (get partially-expanded-value "@value")]
91 | (if (and value (string? value))
92 | ;; Add the (optional) language mapping
93 | (language-mapping {:active-context active-context
94 | :active-property active-property
95 | :value-map partially-expanded-value})
96 | partially-expanded-value))))) ; Non-strings and @id's don't get language mappings
--------------------------------------------------------------------------------
/test/clj_json_ld/unit/iri.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.unit.iri
2 | "
3 | Test IRI expansion as defined here: http://www.w3.org/TR/json-ld-api/#iri-expansion
4 |
5 | Test IRI compaction as defined here: http://www.w3.org/TR/json-ld-api/#iri-compaction
6 | "
7 | (:require [midje.sweet :refer :all]
8 | [clj-json-ld.json-ld :as json-ld]
9 | [clj-json-ld.iri :refer (expand-iri blank-node-identifier? compact-iri?)]))
10 |
11 | (def context {
12 | "@base" "http://base/"
13 | "@vocab" "http://vocab/"
14 | "ex" {"@id" "http://example.org/"}
15 | "" {"@id" "http://empty/"}
16 | "_" {"@id" "http://underscore/"}
17 | "term1" {"@id" "http://example.org/term1"}
18 | })
19 |
20 | (def document-relative {:document-relative true})
21 | (def vocab {:vocab true})
22 |
23 | (facts "about blank node identifiers"
24 |
25 | (facts "we can detect them"
26 | (blank-node-identifier? "_:") => true
27 | (blank-node-identifier? "_:foo") => true)
28 |
29 | (facts "we aren't fooled by fake ones"
30 | (blank-node-identifier? 42) => false
31 | (blank-node-identifier? 3.14) => false
32 | (blank-node-identifier? "foo") => false
33 | (blank-node-identifier? "-:") => false
34 | (blank-node-identifier? "http://cnn.com/") => false
35 | (blank-node-identifier? "http://cnn.com/foo_:") => false
36 | (blank-node-identifier? "http_://cnn.com/foo") => false))
37 |
38 | (facts "about compact IRIs"
39 |
40 | (facts "we can detect them"
41 | (compact-iri? "foo:bar") => true
42 | (compact-iri? "f:b") => true
43 | (compact-iri? "f-o-o:b") => true
44 | (compact-iri? "f:b-a-r") => true)
45 |
46 | (facts "we aren't fooled by fake ones"
47 | (compact-iri? "_:") => false
48 | (compact-iri? "_:foo") => false
49 | (compact-iri? "foobar") => false
50 | (compact-iri? "foo:bar:blat") => false
51 | (compact-iri? "foo:bar:blat:bloo") => false
52 | (compact-iri? "foo foo:bar") => false
53 | (compact-iri? "foo:bar bar") => false
54 | (compact-iri? "foo:") => false
55 | (compact-iri? ":bar") => false))
56 |
57 | (facts "about IRI expansion"
58 |
59 | (facts "with no options"
60 |
61 | (facts "absolute IRI"
62 | (expand-iri context "http://example.org/") => "http://example.org/"
63 | (expand-iri context "ex://foo") => "ex://foo"
64 | (expand-iri context "foo:bar") => "foo:bar")
65 |
66 | (fact "term"
67 | (expand-iri context "ex") => "ex")
68 |
69 | (fact "prefix:suffix"
70 | (expand-iri context "ex:suffix") => "http://example.org/suffix")
71 |
72 | (fact "JSON-LD keyword"
73 | (doseq [json-ld-keyword json-ld/keywords]
74 | (expand-iri context json-ld-keyword) => json-ld-keyword))
75 |
76 | (fact "empty"
77 | (expand-iri context ":suffix") => "http://empty/suffix")
78 |
79 | (fact "unmapped"
80 | (expand-iri context "foo") => "foo")
81 |
82 | (fact "empty term"
83 | (expand-iri context "") => "")
84 |
85 | (fact "blank node"
86 | (expand-iri context "_:t0") => "_:t0")
87 |
88 | (fact "_"
89 | (expand-iri context "_") => "_"))
90 |
91 | (facts "with :document-relative true in the options"
92 |
93 | (fact "absolute IRI"
94 | (expand-iri context "http://example.org/" document-relative) => "http://example.org/"
95 | (expand-iri context "ex://foo" document-relative) => "ex://foo"
96 | (expand-iri context "foo:bar" document-relative) => "foo:bar")
97 |
98 | (fact "term"
99 | (expand-iri context "ex" document-relative) => "http://base/ex")
100 |
101 | (fact "prefix:suffix"
102 | (expand-iri context "ex:suffix" document-relative) => "http://example.org/suffix")
103 |
104 | (fact "JSON-LD keyword"
105 | (doseq [json-ld-keyword json-ld/keywords]
106 | (expand-iri context json-ld-keyword document-relative) => json-ld-keyword))
107 |
108 | (fact "empty"
109 | (expand-iri context ":suffix" document-relative) => "http://empty/suffix")
110 |
111 | (fact "unmapped"
112 | (expand-iri context "foo" document-relative) => "http://base/foo")
113 |
114 | (fact "empty term"
115 | (expand-iri context "" document-relative) => "http://base/")
116 |
117 | (fact "blank node"
118 | (expand-iri context "_:t0" document-relative) => "_:t0")
119 |
120 | (fact "_"
121 | (expand-iri context "_" document-relative) => "http://base/_"))
122 |
123 | (facts "with :vocab true in the options"
124 |
125 | (fact "absolute IRI"
126 | (expand-iri context "http://example.org/" vocab) => "http://example.org/"
127 | (expand-iri context "ex://foo" vocab) => "ex://foo"
128 | (expand-iri context "foo:bar" vocab) => "foo:bar")
129 |
130 | (fact "term"
131 | (expand-iri context "ex" vocab) => "http://example.org/")
132 |
133 | (fact "prefix:suffix"
134 | (expand-iri context "ex:suffix" vocab) => "http://example.org/suffix")
135 |
136 | (fact "JSON-LD keyword"
137 | (doseq [json-ld-keyword json-ld/keywords]
138 | (expand-iri context json-ld-keyword vocab) => json-ld-keyword))
139 |
140 | (fact "empty"
141 | (expand-iri context ":suffix" vocab) => "http://empty/suffix")
142 |
143 | (fact "unmapped"
144 | (expand-iri context "foo" vocab) => "http://vocab/foo")
145 |
146 | (fact "empty term"
147 | (expand-iri context "" vocab) => "http://empty/")
148 |
149 | (fact "blank node"
150 | (expand-iri context "_:t0" vocab) => "_:t0")
151 |
152 | (fact "_"
153 | (expand-iri context "_" vocab) => "http://underscore/")))
--------------------------------------------------------------------------------
/doc/API/clj-json-ld.core.html:
--------------------------------------------------------------------------------
1 |
2 | clj-json-ld.core documentation clj-json-ld.core compact (compact input)(compact input context)(compact input context options)
Compacts the given JSON-LD input according to the steps in the JSON-LD Compaction Algorithm .
Compaction is performed using the provided context. If no context is provided, the input document is compacted using the top-level context of the document.
input - a JSON-LD document as a Unicode text string, or a Map representing a parsed JSON-LD document, or a string URL to a remote JSON-LD document.
context (optional) - a JSON-LD context document as a Unicode text string, or a Map representing a parsed JSON-LD context document, or a string URL to a remote JSON-LD context document. If you want to provide options but not a context, pass in nil for the context.
options (optional) -
Returns a compacted a JSON-LD document as a Unicode text string, or a Map representing the compacted JSON-LD document.
expand (expand input)(expand input options)
Expands the given JSON-LD input according to the steps in the JSON-LD Expansion Algorithm .
input - a JSON-LD document as a Unicode text string, or a Map representing a parsed JSON-LD document, or a string URL to a remote JSON-LD document.
options (optional) -
Returns an expanded JSON-LD document as a Unicode text string, or a Map representing the expanded JSON-LD document.
flatten (flatten input)(flatten input context)(flatten input context options)
Flattens and compacts the given JSON-LD input according to the steps in the JSON-LD Flattening Algorithm .
input - a JSON-LD document as a Unicode text string, or a Map representing a parsed JSON-LD document, or a string URL to a remote JSON-LD document.
context (optional) - a JSON-LD context document as a Unicode text string, or a Map representing a parsed JSON-LD context document, or a string URL to a remote JSON-LD context document. If you want to provide options but not a context, pass in nil for the context.
options (optional) -
Returns a flattened a JSON-LD document as a Unicode text string, or a Map representing the flattened JSON-LD document.
--------------------------------------------------------------------------------
/test/clj_json_ld/unit/value.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.unit.value
2 | "
3 | Test value expansion as defined here: http://www.w3.org/TR/json-ld-api/#iri-expansion
4 |
5 | Test value compaction as defined here: http://www.w3.org/TR/json-ld-api/#iri-compaction
6 | "
7 | (:require [midje.sweet :refer :all]
8 | [clj-json-ld.value :refer (expand-value)]))
9 |
10 | (def context {
11 | "ex" {"@id" "http://example.org/"}
12 | "ex:integer" {"@type" "xsd:integer"}
13 | "ex:double" {"@type" "xsd:double"}
14 | "ex:boolean" {"@type" "xsd:boolean"}
15 | "foaf:knows" {"@type" "@id"}
16 | "foaf:age" {"@type" "xsd:integer"}
17 | "dc:created" {"@type" "xsd:date"}
18 | "ja-lang-ex" {"@language" "ja"}
19 | })
20 |
21 | (def en-context (assoc context "@language" "en"))
22 |
23 | (facts "about value expansion"
24 |
25 | (facts "with no language mapping"
26 | (fact "absolute IRI"
27 | (expand-value context "foaf:knows" "http://example.com/") => {"@id" "http://example.com/"})
28 |
29 | (fact "term"
30 | (expand-value context "foaf:knows" "ex") => {"@id" "ex"})
31 |
32 | (fact "prefix:suffix"
33 | (expand-value context "foaf:knows" "ex:suffix") => {"@id" "http://example.org/suffix"})
34 |
35 | (fact "no IRI"
36 | (expand-value context "foo" "http://example.com/") => {"@value" "http://example.com/"})
37 |
38 | (fact "no term"
39 | (expand-value context "foo" "ex") => {"@value" "ex"})
40 |
41 | (fact "no prefix"
42 | (expand-value context "foo" "ex:suffix") => {"@value" "ex:suffix"})
43 |
44 | (fact "integer"
45 | (expand-value context "foaf:age" "54") => {"@value" "54" "@type" "xsd:integer"})
46 |
47 | (fact "date"
48 | (expand-value context "dc:created" "2011-12-27Z") => {"@value" "2011-12-27Z" "@type" "xsd:date"})
49 |
50 | (fact "native boolean"
51 | (expand-value context "foo" true) => {"@value" true})
52 |
53 | (fact "native integer"
54 | (expand-value context "foo" 1) => {"@value" 1})
55 |
56 | (fact "native double"
57 | (expand-value context "foo" 1.1e1) => {"@value" 1.1E1})
58 |
59 | (fact "native date"
60 | (let [date-stamp (.parse (java.text.SimpleDateFormat. "yyyy-MM-dd") "2011-12-27")]
61 | (expand-value context "foo", date-stamp) => {"@value" date-stamp}))
62 |
63 | (fact "native time"
64 | (let [time-stamp (.parse (java.text.SimpleDateFormat. "HH:mm:ss") "10:11:12Z")]
65 | (expand-value context "foo", time-stamp) => {"@value" time-stamp}))
66 |
67 | (fact "native dateTime"
68 | (let [date-time-stamp (.parse (java.text.SimpleDateFormat. "yyyy-MM-dd'T'HH:mm:ss") "2011-12-27T10:11:12Z")]
69 | (expand-value context "foo" date-time-stamp) => {"@value" date-time-stamp})))
70 |
71 | (facts "with a default @language mapping of en"
72 |
73 | (fact "absolute IRI"
74 | (expand-value context "foaf:knows" "http://example.com/") => {"@id" "http://example.com/"})
75 |
76 | (fact "term"
77 | (expand-value context "foaf:knows" "ex") => {"@id" "ex"})
78 |
79 | (fact "prefix:suffix"
80 | (expand-value context "foaf:knows" "ex:suffix") => {"@id" "http://example.org/suffix"})
81 |
82 | (fact "no IRI"
83 | (expand-value en-context "foo" "http://example.com/") =>
84 | {"@value" "http://example.com/" "@language" "en"})
85 |
86 | (fact "no term"
87 | (expand-value en-context "foo" "ex") => {"@value" "ex" "@language" "en"})
88 |
89 | (fact "no prefix"
90 | (expand-value en-context "foo" "ex:suffix") => {"@value" "ex:suffix" "@language" "en"})
91 |
92 | (fact "native boolean"
93 | (expand-value en-context "foo" true) => {"@value" true})
94 |
95 | (fact "native integer"
96 | (expand-value en-context "foo" 1) => {"@value" 1})
97 |
98 | (fact "native double"
99 | (expand-value en-context "foo" 1.1) => {"@value" 1.1}))
100 |
101 | (facts "with an @language mapping on the active property"
102 | ;; Test these once with no default language and once with 'en' as the default language mapping
103 | (doseq [active-context [context en-context]]
104 |
105 | (fact "no IRI"
106 | (expand-value active-context "ja-lang-ex" "http://example.com/") =>
107 | {"@value" "http://example.com/" "@language" "ja"})
108 |
109 | (fact "no term"
110 | (expand-value active-context "ja-lang-ex" "ex") => {"@value" "ex" "@language" "ja"})
111 |
112 | (fact "no prefix"
113 | (expand-value active-context "ja-lang-ex" "ex:suffix") => {"@value" "ex:suffix" "@language" "ja"})
114 |
115 | (fact "native boolean"
116 | (expand-value active-context "ja-lang-ex" true) => {"@value" true})
117 |
118 | (fact "native integer"
119 | (expand-value active-context "ja-lang-ex" 1) => {"@value" 1})
120 |
121 | (fact "native double"
122 | (expand-value active-context "ja-lang-ex" 1.1) => {"@value" 1.1})))
123 |
124 | (facts "with coerced @type members (that don't match the real type)"
125 |
126 | (fact "boolean-boolean"
127 | (expand-value en-context "ex:boolean" true) => {"@value" true "@type" "xsd:boolean"})
128 |
129 | (fact "boolean-integer"
130 | (expand-value en-context "ex:integer" true) => {"@value" true "@type" "xsd:integer"})
131 |
132 | (fact "boolean-double"
133 | (expand-value en-context "ex:double" true) => {"@value" true "@type" "xsd:double"})
134 |
135 | (fact "double-boolean"
136 | (expand-value en-context "ex:boolean" 1.1) => {"@value" 1.1 "@type" "xsd:boolean"})
137 |
138 | (fact "double-double"
139 | (expand-value en-context "ex:double" 1.1) => {"@value" 1.1 "@type" "xsd:double"})
140 |
141 | (fact "double-integer"
142 | (expand-value en-context "foaf:age" 1.1) => {"@value" 1.1 "@type" "xsd:integer"})
143 |
144 | (fact "integer-boolean"
145 | (expand-value en-context "ex:boolean" 1) => {"@value" 1 "@type" "xsd:boolean"})
146 |
147 | (fact "integer-double"
148 | (expand-value en-context "ex:double" 1) => {"@value" 1 "@type" "xsd:double"})
149 |
150 | (fact "integer-integer"
151 | (expand-value en-context "foaf:age" 1) => {"@value" 1 "@type" "xsd:integer"})
152 |
153 | (fact "string-boolean"
154 | (expand-value en-context "ex:boolean" "foo") => {"@value" "foo" "@type" "xsd:boolean"})
155 |
156 | (fact "string-double"
157 | (expand-value en-context "ex:double" "foo") => {"@value" "foo" "@type" "xsd:double"})
158 |
159 | (fact "string-integer"
160 | (expand-value en-context "foaf:age" "foo") => {"@value" "foo" "@type" "xsd:integer"})))
--------------------------------------------------------------------------------
/src/clj_json_ld/iri.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.iri
2 | "
3 | Implement IRI expansion as defined in the
4 | [IRI Expansion section](http://www.w3.org/TR/json-ld-api/#iri-expansion).
5 |
6 | Implement IRI compaction as defined in the
7 | [IRI Compaction section](http://www.w3.org/TR/json-ld-api/#iri-compaction).
8 | "
9 | (:require [clojure.string :refer (blank? split join)]
10 | [defun :refer (defun-)]
11 | [clojure.core.match :refer (match)]
12 | [clj-json-ld.json-ld :refer (json-ld-keyword?)]
13 | [clojurewerkz.urly.core :as u]))
14 |
15 | ;; Internationalized Resource Identifier (IRI)
16 | ;; http://en.wikipedia.org/wiki/Internationalized_resource_identifier
17 | ;; RFC 3987 http://tools.ietf.org/html/rfc3987
18 |
19 | ;; Example of a compact IRI (foaf:name)
20 | ;; {
21 | ;; "@context":
22 | ;; {
23 | ;; "foaf": "http://xmlns.com/foaf/0.1/"
24 | ;; ...
25 | ;; },
26 | ;; "@type": "foaf:Person"
27 | ;; "foaf:name": "Dave Longley",
28 | ;; ...
29 | ;; }
30 |
31 | (defn- handle-colon [active-context value local-context]
32 | ;; 4.1 Split value into a prefix and suffix at the first occurrence of a colon (:).
33 | (let [parts (split value #":")
34 | prefix (first parts)
35 | suffix (join ":" (rest parts))]
36 |
37 | (match [prefix suffix]
38 |
39 | ;; 4.2) If prefix is underscore (_) or suffix begins with double-forward-slash (//), return value as it is
40 | ;; already an absolute IRI or a blank node identifier.
41 | ["_" _] value
42 | [_ (suffix :guard #(re-find #"^//" %))] value
43 |
44 | ;; 4.3) If local context is not null, it contains a key that equals prefix, and the value associated with the
45 | ;; key that equals prefix in defined is not true, invoke the Create Term Definition algorithm, passing active
46 | ;; context, local context, prefix as term, and defined. This will ensure that a term definition is created for
47 | ;; prefix in active context during Context Processing.
48 |
49 | ;; 4.4) If active context contains a term definition for prefix, return the result of concatenating the IRI
50 | ;; mapping associated with prefix and suffix.
51 | [prefix :guard #(get active-context %) suffix] (str (get-in active-context [prefix "@id"]) suffix)
52 |
53 | ;; 4.5) Return value as it is already an absolute IRI.
54 | :else value)))
55 |
56 | (defun- expand-it
57 |
58 | ;; 1) If value is a keyword or null, return value as is.
59 | ([args :guard #(json-ld-keyword? (:value %))] (:value args))
60 | ([args :guard #(nil? (:value %))] (:value args))
61 |
62 | ;; 2) If local context is not null, it contains a key that equals value, and the value
63 | ;; associated with the key that equals value in defined is not true, invoke the
64 | ;; Create Term Definition algorithm, passing active context, local context, value as term,
65 | ;; and defined.
66 |
67 | ;; 3) If vocab is true and the active context has a term definition for value, return the associated IRI mapping.
68 | ([args :guard #(and (get-in % [:options :vocab]) (get-in % [:active-context (:value %)]))]
69 | (get-in args [:active-context (:value args) "@id"]))
70 |
71 | ; 4) If value contains a colon (:), it is either an absolute IRI ("http://schema.org/name"), a compact
72 | ;; IRI ("foaf:name"), or a blank node identifier ("_:")
73 | ([args :guard #(.contains (:value %) ":")]
74 | (handle-colon (:active-context args) (:value args) (get-in args [:options :local-context])))
75 |
76 | ;; 5) If vocab is true, and active context has a vocabulary mapping, return the result of concatenating the
77 | ;; vocabulary mapping with value.
78 | ([args :guard #(and (get-in % [:options :vocab]) (get-in % [:active-context "@vocab"]))]
79 | (str (get-in args [:active-context "@vocab"]) (:value args)))
80 |
81 | ;; 6) Otherwise, if document relative is true, set value to the result of resolving value against the base IRI.
82 | ;; Only the basic algorithm in section 5.2 of [RFC3986] is used; neither Syntax-Based Normalization nor
83 | ;; Scheme-Based Normalization are performed. Characters additionally allowed in IRI references are treated in the
84 | ;; same way that unreserved characters are treated in URI references, per section 6.5 of [RFC3987].
85 | ([args :guard #(:document-relative (:options %))]
86 | (if-let [base (get-in args [:active-context "@base"])]
87 | (u/resolve base (:value args))
88 | (:value args)))
89 |
90 | ;; 7) Return value as is.
91 | ([args] (:value args)))
92 |
93 | (defn expand-iri
94 | "
95 | [IRI Expansion](http://www.w3.org/TR/json-ld-api/#iri-expansion)
96 |
97 | In JSON-LD documents, some keys and values may represent IRIs. This is an algorithm for
98 | transforming a string that represents an IRI into an absolute IRI or blank node
99 | identifier. It also covers transforming keyword aliases into keywords.
100 |
101 | A blank node identifier is a string that can be used as an identifier for a blank node
102 | within the scope of a JSON-LD document. Blank node identifiers begin with _:
103 |
104 | IRI expansion may occur during context processing or during any of the other
105 | JSON-LD algorithms. If IRI expansion occurs during context processing, then the
106 | local context and its related defined map from the Context Processing algorithm
107 | are passed to this algorithm. This allows for term definition dependencies to be
108 | processed via the Create Term Definition algorithm.
109 |
110 | **active-context** - context map used to resolve terms
111 |
112 | **value** - value to be expanded
113 |
114 | **options** *(optional)* -
115 |
116 | * **:document-relative** - true/false the value can be interpreted as a relative IRI against the documents base IRI, defaults to false
117 | * **:vocab** - true/false the value can be interpreted as a relative IRI against the active context's vocab, defaults to false
118 | * **:local-context** - defaults to nil
119 | * **:defined** - map defaults to nil
120 | "
121 | ([active-context value] (expand-iri active-context value {}))
122 | ([active-context value options]
123 | (expand-it {:active-context active-context :value value :options options})))
124 |
125 | (defn absolute-iri?
126 | "Wrap urly's absolute? in a try because it blows up a lot."
127 | [string]
128 | (try
129 | (u/absolute? string)
130 | (catch Exception e
131 | false))) ; it blew up urly's absolute? so... it's not
132 |
133 | (defn blank-node-identifier?
134 | "A blank node identifier is a string that can be used as an identifier for a blank node within the scope of a
135 | JSON-LD document. Blank node identifiers begin with _:."
136 | [identifier]
137 | (if (and (string? identifier) (re-find #"^_:" identifier)) true false))
138 |
139 | (defn compact-iri?
140 | "
141 | Return a boolean to indicate if the string argument is a compact IRI or not.
142 | http://www.w3.org/TR/json-ld-api/#dfn-compact-iri
143 | "
144 | [string]
145 | (let [parts (split string #":")
146 | prefix (first parts)
147 | suffix (last parts)]
148 | (and
149 | ; have 1 and only 1 colon
150 | (= (count parts) 2)
151 | ; not a blank node identifier
152 | (not (blank-node-identifier? string))
153 | ; have a prefix and suffix that don't have whitespace
154 | (not (re-find #"\s" prefix))
155 | (not (re-find #"\s" suffix))
156 | ; neither the prefix nor the suffix is blank
157 | (not (blank? prefix))
158 | (not (blank? suffix)))))
159 |
160 | ;; http://www.w3.org/TR/json-ld-api/#iri-compaction
161 | ;; compact
--------------------------------------------------------------------------------
/doc/API/css/default.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: Helvetica, Arial, sans-serif;
3 | font-size: 15px;
4 | }
5 |
6 | pre, code {
7 | font-family: Monaco, DejaVu Sans Mono, Consolas, monospace;
8 | font-size: 9pt;
9 | margin: 15px 0;
10 | }
11 |
12 | h2 {
13 | font-weight: normal;
14 | font-size: 28px;
15 | padding: 10px 0 2px 0;
16 | margin: 0;
17 | }
18 |
19 | #header, #content, .sidebar {
20 | position: fixed;
21 | }
22 |
23 | #header {
24 | top: 0;
25 | left: 0;
26 | right: 0;
27 | height: 20px;
28 | background: #444;
29 | color: #fff;
30 | padding: 5px 7px;
31 | }
32 |
33 | #content {
34 | top: 30px;
35 | right: 0;
36 | bottom: 0;
37 | overflow: auto;
38 | background: #fff;
39 | color: #333;
40 | padding: 0 18px;
41 | }
42 |
43 | .sidebar {
44 | position: fixed;
45 | top: 30px;
46 | bottom: 0;
47 | overflow: auto;
48 | }
49 |
50 | #namespaces {
51 | background: #e2e2e2;
52 | border-right: solid 1px #cccccc;
53 | left: 0;
54 | width: 250px;
55 | }
56 |
57 | #vars {
58 | background: #f2f2f2;
59 | border-right: solid 1px #cccccc;
60 | left: 251px;
61 | width: 200px;
62 | }
63 |
64 | .namespace-index {
65 | left: 251px;
66 | }
67 |
68 | .namespace-docs {
69 | left: 452px;
70 | }
71 |
72 | #header {
73 | background: -moz-linear-gradient(top, #555 0%, #222 100%);
74 | background: -webkit-linear-gradient(top, #555 0%, #333 100%);
75 | background: -o-linear-gradient(top, #555 0%, #222 100%);
76 | background: -ms-linear-gradient(top, #555 0%, #222 100%);
77 | background: linear-gradient(top, #555 0%, #222 100%);
78 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.4);
79 | z-index: 100;
80 | }
81 |
82 | #header h1 {
83 | margin: 0;
84 | padding: 0;
85 | font-size: 12pt;
86 | font-weight: lighter;
87 | text-shadow: -1px -1px 0px #333;
88 | }
89 |
90 | #header a, .sidebar a {
91 | display: block;
92 | text-decoration: none;
93 | }
94 |
95 | #header a {
96 | color: #fff;
97 | }
98 |
99 | .sidebar a {
100 | color: #333;
101 | }
102 |
103 | #header h2 {
104 | float: right;
105 | font-size: 9pt;
106 | font-weight: normal;
107 | margin: 3px 3px;
108 | padding: 0;
109 | color: #bbb;
110 | }
111 |
112 | #header h2 a {
113 | display: inline;
114 | }
115 |
116 | .sidebar h3 {
117 | margin: 0;
118 | padding: 10px 10px 0 10px;
119 | font-size: 19px;
120 | font-weight: normal;
121 | }
122 |
123 | .sidebar ul {
124 | padding: 0.5em 0em;
125 | margin: 0;
126 | }
127 |
128 | .sidebar li {
129 | display: block;
130 | vertical-align: middle;
131 | }
132 |
133 | .sidebar li a, .sidebar li .no-link {
134 | border-left: 3px solid transparent;
135 | padding: 0 7px;
136 | white-space: nowrap;
137 | }
138 |
139 | .sidebar li .no-link {
140 | display: block;
141 | color: #777;
142 | font-style: italic;
143 | }
144 |
145 | .sidebar li .inner {
146 | display: inline-block;
147 | padding-top: 7px;
148 | height: 24px;
149 | }
150 |
151 | .sidebar li a, .sidebar li .tree {
152 | height: 31px;
153 | }
154 |
155 | .depth-1 .inner { padding-left: 2px; }
156 | .depth-2 .inner { padding-left: 6px; }
157 | .depth-3 .inner { padding-left: 20px; }
158 | .depth-4 .inner { padding-left: 34px; }
159 | .depth-5 .inner { padding-left: 48px; }
160 | .depth-6 .inner { padding-left: 62px; }
161 |
162 | .sidebar li .tree {
163 | display: block;
164 | float: left;
165 | position: relative;
166 | top: -10px;
167 | margin: 0 4px 0 0;
168 | padding: 0;
169 | }
170 |
171 | .sidebar li.depth-1 .tree {
172 | display: none;
173 | }
174 |
175 | .sidebar li .tree .top, .sidebar li .tree .bottom {
176 | display: block;
177 | margin: 0;
178 | padding: 0;
179 | width: 7px;
180 | }
181 |
182 | .sidebar li .tree .top {
183 | border-left: 1px solid #aaa;
184 | border-bottom: 1px solid #aaa;
185 | height: 19px;
186 | }
187 |
188 | .sidebar li .tree .bottom {
189 | height: 22px;
190 | }
191 |
192 | .sidebar li.branch .tree .bottom {
193 | border-left: 1px solid #aaa;
194 | }
195 |
196 | #namespaces li.current a {
197 | border-left: 3px solid #a33;
198 | color: #a33;
199 | }
200 |
201 | #vars li.current a {
202 | border-left: 3px solid #33a;
203 | color: #33a;
204 | }
205 |
206 | #content h3 {
207 | font-size: 13pt;
208 | font-weight: bold;
209 | }
210 |
211 | .public h3 {
212 | margin: 0;
213 | float: left;
214 | }
215 |
216 | .usage {
217 | clear: both;
218 | }
219 |
220 | .public {
221 | margin: 0;
222 | border-top: 1px solid #e0e0e0;
223 | padding-top: 14px;
224 | padding-bottom: 6px;
225 | }
226 |
227 | .public:last-child {
228 | margin-bottom: 20%;
229 | }
230 |
231 | .members .public:last-child {
232 | margin-bottom: 0;
233 | }
234 |
235 | .members {
236 | margin: 15px 0;
237 | }
238 |
239 | .members h4 {
240 | color: #555;
241 | font-weight: normal;
242 | font-variant: small-caps;
243 | margin: 0 0 5px 0;
244 | }
245 |
246 | .members .inner {
247 | padding-top: 5px;
248 | padding-left: 12px;
249 | margin-top: 2px;
250 | margin-left: 7px;
251 | border-left: 1px solid #bbb;
252 | }
253 |
254 | #content .members .inner h3 {
255 | font-size: 12pt;
256 | }
257 |
258 | .members .public {
259 | border-top: none;
260 | margin-top: 0;
261 | padding-top: 6px;
262 | padding-bottom: 0;
263 | }
264 |
265 | .members .public:first-child {
266 | padding-top: 0;
267 | }
268 |
269 | h4.type,
270 | h4.dynamic,
271 | h4.added,
272 | h4.deprecated {
273 | float: left;
274 | margin: 3px 10px 15px 0;
275 | font-size: 15px;
276 | font-weight: bold;
277 | font-variant: small-caps;
278 | }
279 |
280 | .public h4.type,
281 | .public h4.dynamic,
282 | .public h4.added,
283 | .public h4.deprecated {
284 | font-size: 13px;
285 | font-weight: bold;
286 | margin: 3px 0 0 10px;
287 | }
288 |
289 | .members h4.type,
290 | .members h4.added,
291 | .members h4.deprecated {
292 | margin-top: 1px;
293 | }
294 |
295 | h4.type {
296 | color: #717171;
297 | }
298 |
299 | h4.dynamic {
300 | color: #9933aa;
301 | }
302 |
303 | h4.added {
304 | color: #508820;
305 | }
306 |
307 | h4.deprecated {
308 | color: #880000;
309 | }
310 |
311 | .namespace {
312 | margin-bottom: 30px;
313 | }
314 |
315 | .namespace:last-child {
316 | margin-bottom: 10%;
317 | }
318 |
319 | .index {
320 | padding: 0;
321 | font-size: 80%;
322 | margin: 15px 0;
323 | line-height: 16px;
324 | }
325 |
326 | .index * {
327 | display: inline;
328 | }
329 |
330 | .index p {
331 | padding-right: 3px;
332 | }
333 |
334 | .index li {
335 | padding-right: 5px;
336 | }
337 |
338 | .index ul {
339 | padding-left: 0;
340 | }
341 |
342 | .usage code {
343 | display: block;
344 | color: #008;
345 | margin: 2px 0;
346 | }
347 |
348 | .usage code:first-child {
349 | padding-top: 10px;
350 | }
351 |
352 | p {
353 | margin: 15px 0;
354 | }
355 |
356 | .public p:first-child, .public pre.plaintext {
357 | margin-top: 12px;
358 | }
359 |
360 | .doc {
361 | margin: 0 0 26px 0;
362 | clear: both;
363 | }
364 |
365 | .public .doc {
366 | margin: 0;
367 | }
368 |
369 | .namespace-index .doc {
370 | margin-bottom: 20px;
371 | }
372 |
373 | .namespace-index .namespace .doc {
374 | margin-bottom: 10px;
375 | }
376 |
377 | .markdown {
378 | line-height: 18px;
379 | font-size: 14px;
380 | }
381 |
382 | .doc, .public, .namespace .index {
383 | max-width: 680px;
384 | overflow-x: visible;
385 | }
386 |
387 | .markdown code, .src-link a {
388 | background: #f6f6f6;
389 | border: 1px solid #e4e4e4;
390 | border-radius: 2px;
391 | }
392 |
393 | .markdown pre {
394 | background: #f4f4f4;
395 | border: 1px solid #e0e0e0;
396 | border-radius: 2px;
397 | padding: 5px 10px;
398 | margin: 0 10px;
399 | }
400 |
401 | .markdown pre code {
402 | background: transparent;
403 | border: none;
404 | }
405 |
406 | .doc ul, .doc ol {
407 | padding-left: 30px;
408 | }
409 |
410 | .doc table {
411 | border-collapse: collapse;
412 | margin: 0 10px;
413 | }
414 |
415 | .doc table td, .doc table th {
416 | border: 1px solid #dddddd;
417 | padding: 4px 6px;
418 | }
419 |
420 | .doc table th {
421 | background: #f2f2f2;
422 | }
423 |
424 | .doc dl {
425 | margin: 0 10px 20px 10px;
426 | }
427 |
428 | .doc dl dt {
429 | font-weight: bold;
430 | margin: 0;
431 | padding: 3px 0;
432 | border-bottom: 1px solid #ddd;
433 | }
434 |
435 | .doc dl dd {
436 | padding: 5px 0;
437 | margin: 0 0 5px 10px;
438 | }
439 |
440 | .doc abbr {
441 | border-bottom: 1px dotted #333;
442 | font-variant: none
443 | cursor: help;
444 | }
445 |
446 | .src-link {
447 | margin-bottom: 15px;
448 | }
449 |
450 | .src-link a {
451 | font-size: 70%;
452 | padding: 1px 4px;
453 | text-decoration: none;
454 | color: #5555bb;
455 | }
--------------------------------------------------------------------------------
/src/clj_json_ld/context.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.context
2 | "
3 | Implement the context processing as defined in the
4 | [Context Processing Algorithms section](http://www.w3.org/TR/json-ld-api/#context-processing-algorithm).
5 | "
6 | (:require [defun :refer (defun defun-)]
7 | [clojure.core.match :refer (match)]
8 | [clojure.string :as s]
9 | [clojurewerkz.urly.core :as u]
10 | [clj-json-ld.iri :refer (blank-node-identifier? absolute-iri?)]
11 | [clj-json-ld.term-definition :refer (create-term-definition)]
12 | [clj-json-ld.json-ld-error :refer (json-ld-error)]))
13 |
14 | ;; 3.4) If context has an @base key and remote contexts is empty,
15 | ;; i.e., the currently being processed context is not a remote context:
16 | (defun- process-base-key
17 |
18 | ; currently being processed context IS a remote context, so do nothing
19 | ([result _ remote-contexts :guard #(not (empty? %))]
20 | result)
21 |
22 | ; currently being processed context has a @base key
23 | ([result context :guard #(contains? % "@base") _]
24 |
25 | ;; 3.4.1) Initialize value to the value associated with the @base key.
26 | (let [value (get context "@base")]
27 |
28 | (match [value]
29 |
30 | ;; 3.4.2) If value is null, remove the base IRI of result.
31 | [_ :guard not] (dissoc result "@base")
32 |
33 | ;; 3.4.3) Otherwise, if value is an absolute IRI, the base IRI of result is set to value.
34 | [value :guard absolute-iri?] (assoc result "@base" value)
35 |
36 | ;; 3.4.4) Otherwise, if value is a relative IRI and the base IRI of result is not null,
37 | ;; set the base IRI of result to the result of resolving value against the current base IRI
38 | ;; of result.
39 | [value :guard #(and (string? %) (not (s/blank? (get result "@base"))))]
40 | (assoc result "@base" (u/resolve (get result "@base") value))
41 |
42 | ;; 3.4.5) Otherwise, an invalid base IRI error has been detected and processing is aborted.
43 | :else (json-ld-error "invalid base IRI"
44 | "local context @base has a relative IRI, and there is no absolute @base IRI in the active context"))))
45 |
46 | ; context has no @base key, so do nothing
47 | ([result _ _] result))
48 |
49 | ;; 3.5) If context has an @vocab key:
50 | (defun- process-vocab-key
51 | ; currently being processed context has a @vocab key
52 | ([result context :guard #(contains? % "@vocab")]
53 | ;; 3.5.1) Initialize value to the value associated with the @vocab key.
54 | (if-let [vocab (get context "@vocab")]
55 | ;; 3.5.3) Otherwise, if value is an absolute IRI or blank node identifier,
56 | ;; the vocabulary mapping of result is set to value. If it is not an absolute
57 | ;; IRI or blank node identifier, an invalid vocab mapping error has been detected
58 | ;; and processing is aborted.
59 | (if (and
60 | (string? vocab)
61 | (or (blank-node-identifier? vocab) (absolute-iri? vocab)))
62 | (assoc result "@vocab" vocab)
63 | (json-ld-error "invalid vocab mapping"
64 | "local context has @vocab but it's not an absolute IRI or a blank node identifier"))
65 | ;; 3.5.2) If value is null, remove any vocabulary mapping from result.
66 | (dissoc result "@vocab")))
67 | ; context has no @vocab key, so do nothing
68 | ([result _] result))
69 |
70 |
71 | ;; 3.6) If context has an @language key:
72 | (defun- process-language-key
73 | ; currently being processed context has a @language key
74 | ([result context :guard #(contains? % "@language")]
75 | ;; 3.6.1) Initialize value to the value associated with the @language key.
76 | (if-let [language (get context "@language")]
77 | ;; 3.6.3) Otherwise, if value is string, the default language of result is set to
78 | ;; lowercased value. If it is not a string, an invalid default language error has
79 | ;; been detected and processing is aborted.
80 | (if (string? language)
81 | (assoc result "@language" (s/lower-case language))
82 | (json-ld-error "invalid default language"
83 | "local context has @language but it's not a string"))
84 | ;; 3.6.2) If value is null, remove any language mapping from result.
85 | (dissoc result "@language")))
86 | ; context has no @language key, so do nothing
87 | ([result _] result))
88 |
89 | ;; TODO better/easier than this way to filter keys?
90 | (defn- other-keys
91 | "Return all the keys from a context that are NOT @base, @vocab and @language."
92 | [context]
93 | (->> (keys context)
94 | (filter #(not (or (= "@base" %)(= "@vocab" %)(= "@language" %))))))
95 |
96 | (defun- process-other-keys
97 | "3.8) For each key-value pair in context where key is not @base, @vocab,
98 | or @language, invoke the Create Term Definition algorithm, passing result
99 | for active context, context for local context, key, and defined."
100 |
101 | ;; no more keys in the context to process, return the updated context
102 | ([updated-result _ other-keys :guard empty? _]
103 | updated-result)
104 |
105 | ;; process each key in the context
106 | ([result context other-keys defined]
107 | ;; invoke the Create Term Definition algorithm, passing result for active
108 | ;; context, context for local context, key, and defined.
109 | (let [key (first other-keys)
110 | [updated-result updated-defined] (create-term-definition result context key defined)]
111 | (recur updated-result context (rest other-keys) updated-defined)))
112 |
113 | ;; get the initial set of other keys that we'll be processing and recurse
114 | ([result context] (recur result context (other-keys context) {})))
115 |
116 | ;; 3.4, 3.5, and 3.6) If context IS a JSON object, process the context.
117 | (defn- process-local-context [result context remote-contexts]
118 | (-> result
119 | ;; 3.4) If context has an @base key and remote contexts is empty, i.e., the currently being processed
120 | ;; context is not a remote context:
121 | (process-base-key context remote-contexts)
122 | ;; 3.5) If context has an @vocab key:
123 | (process-vocab-key context)
124 | ;; 3.6) If context has an @language key:
125 | (process-language-key context)
126 | ;; 3.8) For each key-value pair in context where key is not @base, @vocab, or @language...
127 | (process-other-keys context)))
128 |
129 | (defun update-with-local-context
130 | "Update an active context with a local context."
131 |
132 | ;; If remote contexts is not passed, it is initialized to an empty array.
133 | ([active-context local-context]
134 | (update-with-local-context active-context local-context []))
135 |
136 | ;; 2) If local context is not an array, set it to an array containing only local context.
137 | ([active-context local-context :guard #(not (sequential? %)) remote-contexts]
138 | (update-with-local-context active-context [local-context] remote-contexts))
139 |
140 | ;; 1) Initialize result to the result of cloning active context.
141 | ;; Our recursion accumulator starts as a "clone" of the active-context
142 | ([active-context local-context remote-contexts]
143 | (update-with-local-context active-context active-context local-context remote-contexts))
144 |
145 | ;; The next two patterns provide the recursion for:
146 | ;; 3) For each item context in local context
147 |
148 | ;; stop condition of the recursion; return the accumulated result of merging with the local
149 | ;; context(s) as the new active context
150 | ([result _ local-context :guard #(empty? %) _]
151 | result)
152 |
153 | ([result active-context local-context remote-contexts]
154 | (let [context (first local-context)]
155 | (match [context]
156 |
157 | ;; 3.1) If context is null, set result to a newly-initialized active context and continue
158 | ;; with the next context. The base IRI of the active context is set to the IRI of the
159 | ;; currently being processed document (which might be different from the currently being
160 | ;; processed context), if available; otherwise to null. If set, the base option of a JSON-LD
161 | ;; API Implementation overrides the base IRI.
162 | [nil]
163 | (recur {} active-context (rest local-context) remote-contexts)
164 |
165 | ;; 3.2) If context is a string, it's a remote context, retrieve it, parse it and recurse.
166 | [string-context :guard #(string? %)]
167 | (do (println "Remote Context! Do good things here.")
168 | (recur active-context (rest local-context) remote-contexts))
169 |
170 | ;; 3.3) If context is NOT a JSON object, an invalid local context error has been detected and
171 | ;; processing is aborted.
172 | [map-context :guard #(not (map? %))]
173 | (json-ld-error "invalid local context" "local context is not a JSON object")
174 |
175 | ;; 3.4, 3.5, 3.6, and 3.8) If context IS a JSON object, process the context.
176 | [_]
177 | (recur (process-local-context result (first local-context) remote-contexts)
178 | (rest local-context)
179 | remote-contexts)))))
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2014-2015 Snooty Monkey, LLC
2 | http://snootymonkey.com/
3 |
4 | Mozilla Public License
5 | Version 2.0
6 |
7 | 1. Definitions
8 |
9 | 1.1. “Contributor”
10 | means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software.
11 |
12 | 1.2. “Contributor Version”
13 | means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor’s Contribution.
14 |
15 | 1.3. “Contribution”
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. “Covered Software”
19 | means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof.
20 |
21 | 1.5. “Incompatible With Secondary Licenses”
22 | means
23 |
24 | that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or
25 |
26 | that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License.
27 |
28 | 1.6. “Executable Form”
29 | means any form of the work other than Source Code Form.
30 |
31 | 1.7. “Larger Work”
32 | means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software.
33 |
34 | 1.8. “License”
35 | means this document.
36 |
37 | 1.9. “Licensable”
38 | means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License.
39 |
40 | 1.10. “Modifications”
41 | means any of the following:
42 |
43 | any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or
44 |
45 | any new file in Source Code Form that contains any Covered Software.
46 |
47 | 1.11. “Patent Claims” of a Contributor
48 | means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version.
49 |
50 | 1.12. “Secondary License”
51 | means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses.
52 |
53 | 1.13. “Source Code Form”
54 | means the form of the work preferred for making modifications.
55 |
56 | 1.14. “You” (or “Your”)
57 | means an individual or a legal entity exercising rights under this License. For legal entities, “You” includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity.
58 |
59 | 2. License Grants and Conditions
60 |
61 | 2.1. Grants
62 |
63 | Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license:
64 |
65 | under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and
66 |
67 | under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version.
68 |
69 | 2.2. Effective Date
70 |
71 | The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution.
72 |
73 | 2.3. Limitations on Grant Scope
74 |
75 | The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor:
76 |
77 | for any code that a Contributor has removed from Covered Software; or
78 |
79 | for infringements caused by: (i) Your and any other third party’s modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or
80 |
81 | under Patent Claims infringed by Covered Software in the absence of its Contributions.
82 |
83 | This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4).
84 |
85 | 2.4. Subsequent Licenses
86 |
87 | No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3).
88 |
89 | 2.5. Representation
90 |
91 | Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License.
92 |
93 | 2.6. Fair Use
94 |
95 | This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents.
96 |
97 | 2.7. Conditions
98 |
99 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1.
100 |
101 | 3. Responsibilities
102 |
103 | 3.1. Distribution of Source Form
104 |
105 | All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients’ rights in the Source Code Form.
106 |
107 | 3.2. Distribution of Executable Form
108 |
109 | If You distribute Covered Software in Executable Form then:
110 |
111 | such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and
112 |
113 | You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients’ rights in the Source Code Form under this License.
114 |
115 | 3.3. Distribution of a Larger Work
116 |
117 | You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s).
118 |
119 | 3.4. Notices
120 |
121 | You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies.
122 |
123 | 3.5. Application of Additional Terms
124 |
125 | You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction.
126 |
127 | 4. Inability to Comply Due to Statute or Regulation
128 |
129 | If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it.
130 |
131 | 5. Termination
132 |
133 | 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice.
134 |
135 | 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate.
136 |
137 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination.
138 |
139 | 6. Disclaimer of Warranty
140 |
141 | Covered Software is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer.
142 |
143 | 7. Limitation of Liability
144 |
145 | Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party’s negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You.
146 |
147 | 8. Litigation
148 |
149 | Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party’s ability to bring cross-claims or counter-claims.
150 |
151 | 9. Miscellaneous
152 |
153 | This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor.
154 |
155 | 10. Versions of the License
156 |
157 | 10.1. New Versions
158 |
159 | Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number.
160 |
161 | 10.2. Effect of New Versions
162 |
163 | You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward.
164 |
165 | 10.3. Modified Versions
166 |
167 | If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License).
168 |
169 | 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
170 |
171 | If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached.
172 |
173 | Exhibit A - Source Code Form License Notice
174 |
175 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
176 |
177 | If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice.
178 |
179 | You may add additional accurate notices of copyright ownership.
180 |
181 | Exhibit B - “Incompatible With Secondary Licenses” Notice
182 |
183 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | --= clj-json-ld =--
2 | =====================
3 |
4 | [](https://www.mozilla.org/MPL/2.0/)
5 | [](https://travis-ci.org/SnootyMonkey/clj-json-ld)
6 |
7 | > "Data is messy and disconnected. JSON-LD organizes and connects it, creating a better Web."
8 |
9 | clj-json-ld is the Clojure library for [JSON-LD](http://json-ld.org/) (JavaScript Object Notation for Linking Data).
10 |
11 | * [Introduction](#introduction)
12 | * [Benefits of Linked Data](#benefits-of-linked-data)
13 | * [Benefits of JSON-LD](#benefits-of-json-ld)
14 | * [JSON-LD 101](#json-ld-101)
15 | * [Capabilities of clj-json-ld](#capabilities-of-clj-json-ld)
16 | * [Installation](#installation)
17 | * [Usage](#usage)
18 | * [Testing](#testing)
19 | * [Development and Contributing](#development-and-contributing)
20 | * [Branches](#branches)
21 | * [Acknowledgements](#acknowledgements)
22 | * [License](#license)
23 |
24 | ## Introduction
25 |
26 | clj-json-ld implements the [JSON LD 1.0 Processing Algorithms and API](http://www.w3.org/TR/json-ld-api/)
27 | as a [conforming JSON-LD Processor](http://www.w3.org/TR/json-ld-api/#conformance).
28 |
29 |
30 | ### Benefits of Linked Data
31 |
32 | [Linked Data](http://www.w3.org/DesignIssues/LinkedData.html) is a way to create a network of standards-based machine interpretable data across different documents, APIs and Web sites. It allows an application to start at one piece of Linked Data, and follow embedded links to other pieces of Linked Data that come from different sites and APIs across the Web.
33 |
34 |
35 | ### Benefits of JSON-LD
36 |
37 | JSON-LD is a lightweight syntax to serialize Linked Data in [JSON](http://www.ietf.org/rfc/rfc4627.txt). Its design allows existing JSON to be interpreted as Linked Data with minimal changes.
38 |
39 | JSON-LD is a way to use Linked Data in API programming environments, to build interoperable APIs, and to store Linked Data in JSON-based database engines (most NoSQL databases). Since JSON-LD is 100% compatible with JSON, the large number of JSON parsers, libraries and databases available today can be reused.
40 |
41 | JSON-LD provides:
42 |
43 | * a universal identifier mechanism for JSON objects,
44 | * a way to disambiguate keys shared among different JSON documents,
45 | * a mechanism in which a value in a JSON object may refer to a JSON object on a different site on the Web,
46 | * the ability to annotate strings in JSON objects with their language,
47 | * a way to associate data types with string values such as dates and times,
48 | * and a facility to express one or more directed graphs, such as a social network, or a taxonomy, in a single document.
49 |
50 | The JSON-LD syntax is designed to not disturb already deployed systems running on JSON, and provides a smooth upgrade path from JSON to JSON-LD. Since the shape of JSON data varies wildly, JSON-LD features mechanisms to reshape documents into a deterministic structure which simplifies their processing.
51 |
52 | JSON-LD is designed to be usable directly as JSON, with no knowledge of [RDF](http://www.w3.org/TR/2014/PR-rdf11-concepts-20140109/). It is an alternative syntax for the same underlying data model as RDF, so it is also designed to be transformable to/from RDF, if desired, for use with other Linked Data technologies like [SPARQL](http://www.w3.org/TR/rdf-sparql-query/).
53 |
54 | Developers who require any of the facilities listed above or need to serialize/deserialize an RDF Graph or RDF Dataset in a JSON-based syntax will find JSON-LD of interest.
55 |
56 |
57 | ### JSON-LD 101
58 |
59 | Let's take a look at some very simple JSON about a book that you might get back from an API:
60 |
61 | ```json
62 | {
63 | "name": "Myth of Sisyphus",
64 | "author": "Albert Camus",
65 | "location": "http://amazon.com/Myth-Sisyphus-Albert-Camus/dp/7500133340/",
66 | "image": "http://ecx.images-amazon.com/images/I/61hJVrZgBBL.jpg"
67 | }
68 | ```
69 |
70 | A different API might provide this JSON about the very same book:
71 |
72 | ```json
73 | {
74 | "author": "Albert Camus",
75 | "title": "Myth of Sisyphus",
76 | "image": "myth.png",
77 | "location": "3rd Floor, Manning Bldg.",
78 | "lang": "en-US"
79 | }
80 | ```
81 |
82 | As a human, it's easy to deduce what this is all about. We have the title of the book, the author, and different locations and images of the book.
83 |
84 | As a computer algorithm however, this is all quite vague. Is `name` the same as `title`? Is `location` a place or a URL? Is `image` the name of a file? A full URL? A relative URL? A base-64 encoded image? What's `lang` mean?
85 |
86 | The same JSON documents converted to JSON-LD documents remove much of this ambiguity:
87 |
88 | ```json
89 | {
90 | "http://www.schema.org/name": "Myth of Sisyphus",
91 | "http://www.schema.org/author": "Albert Camus",
92 | "http://www.schema.org/url": {"@id": "http://amazon.com/Myth-Sisyphus-Albert-Camus/dp/7500133340/"}, ← The '@id' keyword means 'This value is an identifier that is an IRI (URL)'
93 | "http://www.schema.org/image": {"@id": "http://ecx.images-amazon.com/images/I/61hJVrZgBBL.jpg"} ← The '@id' keyword means 'This value is an identifier that is an IRI (URL)'
94 | }
95 | ```
96 |
97 | and:
98 |
99 | ```json
100 | {
101 | "http://www.schema.org/author": "Albert Camus",
102 | "http://www.schema.org/name": "Myth of Sisyphus",
103 | "http://www.schema.org/image": "myth.png",
104 | "http://www.schema.org/contentLocation": "3rd Floor, Manning Library",
105 | "http://www.schema.org/inLanguage": "en-US"
106 | }
107 | ```
108 |
109 | Our ambiguity is cleared up nicely by using JSON-LD documents. The `@id` term tells us that `image` is a URL in one case, but not in the other. Using a common schema, [schema.org's book schema](http://www.schema.org/Book) in this case, has brought together `name` and `title` as referring to the same thing, but the `location` values as referring to different things, a virtual location in one case and a physical location in the other.
110 |
111 | But who wants their JSON documents to look like this? A computer algorithm that's dealing with JSON documents from different sources does, but you don't want to read and write your JSON documents like this all the time. With JSON-LD we can move this metadata out of the JSON document and into a context.
112 |
113 | The contexts for the above JSON-LD documents look like this:
114 |
115 | ```json
116 | {
117 | "@context": {
118 | "name": "http://schema.org/name", ← This means that 'name' is shorthand for 'http://schema.org/name'
119 | "author": "http://www.schema.org/author", ← This means that 'author' is shorthand for 'http://schema.org/author'
120 | "location": {
121 | "@id": "http://schema.org/url", ← This means that 'location' is shorthand for 'http://schema.org/url'
122 | "@type": "@id" ← This means that a string value associated with 'location' should be interpreted as an identifier that is an IRI (a URL)
123 | },
124 | "image": {
125 | "@id": "http://schema.org/image", ← This means that 'image' is shorthand for 'http://schema.org/image'
126 | "@type": "@id" ← This means that a string value associated with 'image' should be interpreted as an identifier that is an IRI (a URL)
127 | }
128 | }
129 | }
130 | ```
131 |
132 | and:
133 |
134 | ```json
135 | {
136 | "@context": {
137 | "author": "http://www.schema.org/author", ← This means that 'author' is shorthand for 'http://schema.org/author'
138 | "title": "http://www.schema.org/name", ← This means that 'title' is shorthand for 'http://schema.org/name'
139 | "image": "http://schema.org/image", ← This means that 'image' is shorthand for 'http://schema.org/image', and is not an IRI (a URL)
140 | "location": "http://www.schema.org/contentLocation", ← This means that 'location' is shorthand for 'http://schema.org/contentLocation', and is not an IRI (a URL)
141 | "lang": "http://www.schema.org/inLanguage" ← This means that 'lang' is shorthand for 'http://schema.org/inLanguage'
142 | }
143 | }
144 | ```
145 |
146 | If that context is then embedded in its own section of the JSON-LD document, or placed in an accessible location online, say at `http://the-site.org/contexts/book.jsonld`, then the original simpler JSON can be used, but with the semantic ambiguity removed. This is achieved with the addition of a `@context` property to make it a JSON-LD document.
147 |
148 | The context can be included by reference:
149 |
150 | ```json
151 | {
152 | "@context": "http://the-site.org/contexts/book.jsonld",
153 | "name": "Myth of Sisyphus",
154 | "author": "Albert Camus",
155 | "location": "http://amazon.com/Myth-Sisyphus-Albert-Camus/dp/7500133340/",
156 | "image": "http://ecx.images-amazon.com/images/I/61hJVrZgBBL.jpg"
157 | }
158 | ```
159 |
160 | Or the context can be directly embedded:
161 |
162 | ```json
163 | {
164 | "@context": {
165 | "author": "http://www.schema.org/author",
166 | "title": "http://www.schema.org/name",
167 | "image": "http://schema.org/image",
168 | "location": "http://www.schema.org/contentLocation",
169 | "lang": "http://www.schema.org/inLanguage"
170 | },
171 | "author": "Albert Camus",
172 | "title": "Myth of Sisyphus",
173 | "image": "myth.png",
174 | "location": "3rd Floor, Manning Bldg.",
175 | "lang": "en-US"
176 | }
177 | ```
178 |
179 | Or the context can be left out completely and provided by an HTTP Link Header:
180 |
181 | ```
182 | HTTP/1.1 200 OK
183 | Content-Type: application/json
184 | Link: ; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"
185 |
186 | {
187 | "name": "Myth of Sisyphus",
188 | "author": "Albert Camus",
189 | "location": "http://amazon.com/Myth-Sisyphus-Albert-Camus/dp/7500133340/",
190 | "image": "http://ecx.images-amazon.com/images/I/61hJVrZgBBL.jpg"
191 | }
192 | ```
193 |
194 | Or maybe the JSON provider is not interested in providing JSON-LD in any form. In that case, we can create the context for the JSON data ourselves and provide it directly to the JSON-LD processor.
195 |
196 |
197 | ### Capabilities of clj-json-ld
198 |
199 | A JSON-LD processor, like clj-json-ld, helps you transform your JSON-LD documents in between different valid JSON-LD formats, some more explicit for semantic reasoning by algorithms, and others more readable and compact for humans and for transmission.
200 |
201 | clj-json-ld can perform the [expansion](http://www.w3.org/TR/json-ld/#expanded-document-form), [compaction](http://www.w3.org/TR/json-ld/#compacted-document-form), and [flattening](http://www.w3.org/TR/json-ld/#flattened-document-form) operations defined in the [expansion](http://www.w3.org/TR/json-ld-api/#expansion-algorithm), [compaction](http://www.w3.org/TR/json-ld-api/#compaction-algorithm) and [flattening](http://www.w3.org/TR/json-ld-api/#flattening-algorithm) sections of the [processing specification](http://www.w3.org/TR/json-ld-api/).
202 |
203 | :memo: Show the document expanded, compacted and flattened.
204 |
205 |
206 | ## Installation
207 |
208 | :bomb: This library is currently a WIP stub and is not yet usable. Stay tuned!
209 |
210 |
211 | ## Usage
212 |
213 | At the Clojure REPL:
214 |
215 | ```clojure
216 | (require '[clj-json-ld.core :as json-ld])
217 | ```
218 |
219 | In your Clojure namespace:
220 |
221 | ```clojure
222 | (ns your-app.core
223 | (:require [clj-json-ld.core :as :json-ld]))
224 | ```
225 |
226 | :warning: The `flatten` function specified in [JSON-LD 1.0 Processing Algorithms and API](http://www.w3.org/TR/json-ld-api/) conflicts with Clojure core's [flatten](https://clojuredocs.org/clojure.core/flatten) function, so unless you use `:as` in your require, or exclude Clojure's `flatten` from your namespace with `(:refer-clojure :exclude [flatten])`, you'll see a warning about the replacement:
227 |
228 | ```
229 | WARNING: flatten already refers to: #'clojure.core/flatten in namespace: user, being replaced by: #'clj-json-ld.core/flatten
230 | ```
231 |
232 | More detailed usage instructions are in the API documentation.
233 |
234 |
235 | ## Testing
236 |
237 | The [JSON-LD Test Suite](http://json-ld.org/test-suite/) is used to verify [JSON-LD Processor conformance](http://json-ld.org/test-suite/reports/). clj-json-ld uses the JSON-LD Test Suite, as well as some additional tests.
238 |
239 | Tests are run in continuous integration of the `master` and `dev` branches on [Travis CI](https://travis-ci.org/path/posthere.io):
240 |
241 | [](https://travis-ci.org/SnootyMonkey/clj-json-ld)
242 |
243 | To run the tests locally, you need to clone the [JSON-LD tests GitHub repository](https://github.com/json-ld/tests) as a peer directory to this repository:
244 |
245 | ```console
246 | git clone https://github.com/json-ld/tests json-ld-tests
247 | ```
248 |
249 | Then run the tests from this repository with:
250 |
251 | ```console
252 | lein midje
253 | ```
254 |
255 | ## Development and Contributing
256 |
257 | If you'd like to enhance clj-json-ld, please fork it [on GitHub](https://github.com/SnootyMonkey/clj-json-ld). If you'd like to contribute back your enhancements (awesome!), please submit your pull requests to the `dev` branch. I promise to look at every pull request and incorporate it, or at least provide feedback on why if I won't.
258 |
259 | * Do your best to conform to the coding style that's here... I like it.
260 | * Use 2 soft spaces for indentation.
261 | * Don't leave trailing spaces after lines.
262 | * Don't leave trailing new lines at the end of files.
263 | * Write comments.
264 | * Write tests.
265 | * Don't submit über pull requests, keep your changes atomic.
266 | * Have fun!
267 |
268 |
269 | ### Branches
270 |
271 | There are 2 long lived branches in the repository:
272 |
273 | [master](https://github.com/SnootyMonkey/clj-json-ld/tree/master) - mainline, picked up by [continual integration on Travis-CI](https://travis-ci.org/SnootyMonkey/clj-json-ld), named releases are tagged with: vX.X.X
274 |
275 | [dev](https://github.com/SnootyMonkey/clj-json-ld/tree/dev) - development mainline, picked up by [continual integration on Travis-CI](https://travis-ci.org/SnootyMonkey/clj-json-ld)
276 |
277 | Additional short lived feature branches will come and go.
278 |
279 |
280 | ## Acknowledgements
281 |
282 | Thank you to the creators of the [JSON-LD 1.0 W3C Recommendation](http://www.w3.org/TR/json-ld/) for their hard work in creating the specification and the comprehensive test suite. Thank you to the authors and editors for the very clear spec writing.
283 |
284 | Portions of the [Benefits of Linked Data](#benefits-of-linked-data) and [Benefits of JSON-LD](#benefits-of-json-ld) sections of this README document are lifted with slight modifications from the [JSON-LD 1.0 W3C Recommendation](http://www.w3.org/TR/json-ld/).
285 |
286 | Thank you to [Gregg Kellog](https://github.com/gkellogg), author of the [json-ld Ruby processor](https://github.com/ruby-rdf/json-ld/), and [Dave Longley](https://github.com/dlongley), author of the [pyld Python processor](https://github.com/digitalbazaar/pyld), for providing prior implementations that were useful to study, particularly in the area of testing.
287 |
288 |
289 | ## License
290 |
291 | clj-json-ld is distributed under the [Mozilla Public License v2.0](http://www.mozilla.org/MPL/2.0/).
292 |
293 | Copyright © 2014-2015 [Snooty Monkey, LLC](http://snootymonkey.com/)
--------------------------------------------------------------------------------
/test/clj_json_ld/unit/context.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.unit.context
2 | "
3 | Test the context processing as defined here: http://www.w3.org/TR/json-ld-api/#context-processing-algorithm
4 | "
5 | (:require [clojure.string :as s]
6 | [midje.sweet :refer :all]
7 | [clj-json-ld.json-ld :as json-ld]
8 | [clj-json-ld.context :refer (update-with-local-context)]
9 | [clj-json-ld.iri :refer (expand-iri)]
10 | [clojurewerkz.urly.core :as u]))
11 |
12 | (def fcms-iri "http://falkland-cms.com/")
13 | (def falklandsophile-iri "http://falklandsophile.com/")
14 | (def snootymonkey-iri "http://snootymonkey.com")
15 | (def relative-iri "/foo/bar")
16 |
17 | (def vocab-iri "http://vocab.org/")
18 | (def another-vocab-iri "http://vocab.net/")
19 |
20 | (def language "x-l337-5p34k")
21 | (def another-language "en-Kelper")
22 |
23 | (def blank-node-identifier "_:foo")
24 |
25 | (def not-strings [42 3.14 [] {} () #{}])
26 |
27 | (def active-context {
28 | "@base" fcms-iri
29 | "@vocab" "http://vocab.com/"
30 | "@language" "x-pig-latin"
31 | "@foo" :bar
32 | })
33 |
34 | (facts "about updating active context with local contexts"
35 |
36 | (facts "about invalid local contexts"
37 |
38 | (facts "as a scalar"
39 | (update-with-local-context active-context 1) => (throws clojure.lang.ExceptionInfo)
40 | (update-with-local-context active-context [{} "" nil 1]) => (throws clojure.lang.ExceptionInfo)
41 | (update-with-local-context active-context 1.1) => (throws clojure.lang.ExceptionInfo)
42 | (update-with-local-context active-context [{} "" nil 1.1]) => (throws clojure.lang.ExceptionInfo))
43 |
44 | (facts "as a keyword"
45 | (update-with-local-context active-context :foo) => (throws clojure.lang.ExceptionInfo)
46 | (update-with-local-context active-context [{} "" nil :foo]) => (throws clojure.lang.ExceptionInfo)
47 | (update-with-local-context active-context :foo) => (throws clojure.lang.ExceptionInfo)
48 | (update-with-local-context active-context [{} "" nil :foo]) => (throws clojure.lang.ExceptionInfo))
49 |
50 | (facts "as a sequential"
51 | (update-with-local-context active-context [[]]) => (throws clojure.lang.ExceptionInfo)
52 | (update-with-local-context active-context [{} "" nil []]) => (throws clojure.lang.ExceptionInfo)
53 | (update-with-local-context active-context [()]) => (throws clojure.lang.ExceptionInfo)
54 | (update-with-local-context active-context [{} "" nil ()]) => (throws clojure.lang.ExceptionInfo)))
55 |
56 | (facts "about nil local contexts"
57 |
58 | (fact "result in a newly-initialized active context"
59 | (update-with-local-context active-context nil) => {}
60 | (update-with-local-context active-context [nil]) => {}
61 | (update-with-local-context active-context [{:blat "bloo"} nil]) => {}))
62 |
63 | (future-facts "about local contexts as remote strings")
64 |
65 | (facts "about @base in local contexts"
66 |
67 | (facts "@base of nil removes the @base"
68 | (update-with-local-context active-context {"@base" nil}) => (dissoc active-context "@base"))
69 |
70 | (facts "@base of an absolute IRI makes the IRI the @base"
71 |
72 | (doseq [local-context [
73 | {"@base" falklandsophile-iri}
74 | [{} {"@base" falklandsophile-iri}]
75 | [{"@base" falklandsophile-iri} {}]
76 | [{"@base" snootymonkey-iri} {"@base" falklandsophile-iri} {}]
77 | [{"@base" snootymonkey-iri} {"@base" fcms-iri} {} {"@base" nil} {"@base" falklandsophile-iri}]
78 | ]]
79 | (update-with-local-context active-context local-context) =>
80 | (assoc active-context "@base" falklandsophile-iri)))
81 |
82 | (facts "@base of an relative IRI merges with the @base of the active-context"
83 |
84 | (update-with-local-context active-context {"@base" "foo/bar"}) =>
85 | (assoc active-context "@base" (u/resolve fcms-iri "foo/bar"))
86 |
87 | (update-with-local-context active-context [{} {"@base" "foo/bar"} {}]) =>
88 | (assoc active-context "@base" (u/resolve fcms-iri "foo/bar"))
89 |
90 | (update-with-local-context active-context [{"@base" "foo/bar"} {"@base" "bloo/blat"}]) =>
91 | (assoc active-context "@base" (-> fcms-iri (u/resolve "foo/bar") (u/resolve "bloo/blat"))))
92 |
93 | (facts "@base of a relative IRI without an @base in the active-context is an invalid base IRI error"
94 |
95 | (doseq [local-context (concat
96 | (map #(hash-map "@base" %) not-strings)
97 | [
98 | {"@base" "foo/bar"}
99 | [{"@base" nil} {"@base" "foo/bar"}]
100 | [{} {"@base" "foo/bar"}]
101 | [{"@base" "foo/bar"} {"@base" falklandsophile-iri}]
102 | ])]
103 | (update-with-local-context {} local-context) => (throws clojure.lang.ExceptionInfo))))
104 |
105 | (facts "about @vocab in local contexts"
106 |
107 | (facts "@vocab of nil removes the @vocab"
108 | (update-with-local-context active-context {"@vocab" nil}) => (dissoc active-context "@vocab"))
109 |
110 | (facts "@vocab of an absolute IRI makes the IRI the @vocab"
111 |
112 | (doseq [local-context [
113 | {"@vocab" vocab-iri}
114 | [{} {"@vocab" vocab-iri}]
115 | [{"@vocab" vocab-iri} {}]
116 | [{"@vocab" another-vocab-iri} {"@vocab" vocab-iri} {}]
117 | [{"@vocab" another-vocab-iri} {} {"@vocab" nil} {"@vocab" vocab-iri}]
118 | ]]
119 | (update-with-local-context active-context local-context) =>
120 | (assoc active-context "@vocab" vocab-iri)))
121 |
122 | (facts "@vocab of a blank node identifier makes the blank node identifier the @vocab"
123 | (doseq [local-context [
124 | {"@vocab" blank-node-identifier}
125 | [{} {"@vocab" blank-node-identifier}]
126 | [{"@vocab" blank-node-identifier} {}]
127 | [{"@vocab" another-vocab-iri} {"@vocab" vocab-iri} {"@vocab" blank-node-identifier} {}]
128 | [{"@vocab" another-vocab-iri} {"@vocab" vocab-iri} {} {"@vocab" nil} {"@vocab" blank-node-identifier}]
129 | ]]
130 | (update-with-local-context active-context local-context) =>
131 | (assoc active-context "@vocab" blank-node-identifier)))
132 |
133 | (facts "@vocab of anything else is an invalid vocab mapping"
134 |
135 | (doseq [local-context (concat
136 | (map #(hash-map "@base" %) not-strings)
137 | [
138 | {"@vocab" "foo"}
139 | {"@vocab" "foo/bar"}
140 | [{"@vocab" nil} {"@vocab" "foo/bar"}]
141 | [{} {"@vocab" "foo/bar"}]
142 | [{"@base" vocab-iri} {"@vocab" "foo/bar"} {"@base" another-vocab-iri}]
143 | ])]
144 | (update-with-local-context {} local-context) => (throws clojure.lang.ExceptionInfo))))
145 |
146 | (facts "about @language in local contexts"
147 |
148 | (facts "@language of nil removes the @language"
149 | (update-with-local-context active-context {"@language" nil}) => (dissoc active-context "@language"))
150 |
151 | (facts "@language of any string makes the string the @language"
152 |
153 | (doseq [local-context [
154 | {"@language" language}
155 | [{} {"@language" language}]
156 | [{"@language" language} {}]
157 | [{"@language" another-language} {"@language" language} {}]
158 | [{"@language" another-language} {} {"@language" nil} {"@language" language}]
159 | ]]
160 | (update-with-local-context active-context local-context) =>
161 | (assoc active-context "@language" language))
162 |
163 | (fact "@language string is lower-cased to makes the @language"
164 | (update-with-local-context active-context {"@language" another-language}) =>
165 | (assoc active-context "@language" (s/lower-case another-language))))
166 |
167 | (facts "@language of any non-string invalid default language"
168 | (doseq [value not-strings]
169 | (update-with-local-context {} {"@language" value}) => (throws clojure.lang.ExceptionInfo))))
170 |
171 | (facts "about additional terms in local contexts"
172 |
173 | (facts "a term defined as nil in the local context is nil in the active context"
174 | (update-with-local-context active-context {"@bar" nil}) => (assoc active-context "@bar" nil)
175 | (update-with-local-context active-context {"@bar" {"@id" nil "foo" "bar"}}) =>
176 | (assoc active-context "@bar" nil))
177 |
178 | (facts "a term in the local context that's a JSON-LD keyword is a keyword redefinition error"
179 | (doseq [json-ld-keyword (disj json-ld/keywords "@base" "@vocab" "@language")]
180 | (update-with-local-context active-context {json-ld-keyword "http://abs.com"}) =>
181 | (throws clojure.lang.ExceptionInfo)))
182 |
183 | (facts "a term defined as a string in the local context is a JSON object with an @id in the active context"
184 | (update-with-local-context active-context {"@foo" "bar"}) => (assoc active-context "@foo" {"@id" "bar"})
185 | (update-with-local-context active-context {"@foo" "bar" "@blat" "bloo"}) =>
186 | (assoc active-context "@foo" {"@id" "bar"} "@blat" {"@id" "bloo"}))
187 |
188 | (facts "a term defined as anything but a string or a JSON object is an invalid term definition"
189 | (doseq [value (remove not-strings {})]
190 | (update-with-local-context active-context {"@foo" value}) => (throws clojure.lang.ExceptionInfo)))
191 |
192 | (facts "about @type values in a defined term"
193 |
194 | (facts "a term defined with a valid @type mapping adds the term and the @type mapping to the active context"
195 | (doseq [type-value ["@id" "@vocab" "bar" "foo:bar" fcms-iri]]
196 | (let [expanded-type-value (expand-iri active-context type-value {
197 | :vocab true
198 | :document-relative false
199 | :local-context {}
200 | :defined {}})]
201 | (update-with-local-context active-context {"foo" {"@type" type-value}}) =>
202 | (assoc active-context "foo" {"@type" expanded-type-value "@id" "http://vocab.com/foo"}))))
203 |
204 | (facts "a term defined with a invalid @type mapping is an invalid type mapping"
205 | (doseq [type (concat not-strings [blank-node-identifier "@foo" "@container"])]
206 | (update-with-local-context active-context {"foo" {"@type" type}}) => (throws clojure.lang.ExceptionInfo))))
207 |
208 | (facts "about @reverse values in a defined term"
209 |
210 | (facts "a term defined with a valid @reverse adds the term and the expanded @reverse mapping to the term definition in the active context"
211 | (doseq [reverse-value [blank-node-identifier "bar" "foo:bar" fcms-iri]]
212 | (update-with-local-context active-context {"foo" {"@reverse" "bar"}}) =>
213 | (assoc active-context "foo" {:reverse true "@reverse" "http://vocab.com/bar" "@id" "http://vocab.com/foo"})))
214 |
215 | (fact "a term defined with @reverse and a valid @container of @index or @set, adds them to the active context"
216 | (doseq [container-value ["@index", "@set"]]
217 | (update-with-local-context active-context {"foo" {"@reverse" fcms-iri "@container" container-value}}) =>
218 | (assoc active-context "foo" {
219 | :reverse true
220 | "@reverse" fcms-iri
221 | "@container" container-value
222 | "@id" "http://vocab.com/foo"})))
223 |
224 | (fact "a term defined with @reverse and @id is an invalid reverse property"
225 | (update-with-local-context active-context {"foo" {"@reverse" "foo" "@id" "foo"}}) => (throws clojure.lang.ExceptionInfo))
226 |
227 | (fact "a term defined with @reverse and @container that is not @index, @set or nil is an invalid reverse property"
228 | (doseq [container-value (concat not-strings ["foo" "@id"])]
229 | (update-with-local-context active-context {"foo" {"@reverse" "foo" "@container" container-value}}) =>
230 | (throws clojure.lang.ExceptionInfo)))
231 |
232 | (fact "a term defined with an @reverse value that's not a string is an invalid IRI mapping"
233 | (doseq [value not-strings]
234 | (update-with-local-context active-context {"foo" {"@reverse" value}}) => (throws clojure.lang.ExceptionInfo))))
235 |
236 | (facts "about iri mapping"
237 |
238 | ;; 13.x
239 | (facts "when term has an @id that isn't the term"
240 |
241 | (facts "a term defined with a valid @id adds the expanded IRI to the term definition in the active context"
242 | (doseq [id-value ["bar" "foo:bar" fcms-iri]]
243 | (update-with-local-context active-context {"foo" {"@id" id-value}}) =>
244 | (assoc active-context "foo"
245 | {"@id" (expand-iri active-context id-value {
246 | :vocab true
247 | :document-relative false
248 | :local-context {}
249 | :defined {}})})))
250 |
251 | (facts "and the value of @id is not a string it is an invalid IRI mapping"
252 | (doseq [id-value not-strings]
253 | (update-with-local-context active-context {"foo" {"@id" id-value}}) =>
254 | (throws clojure.lang.ExceptionInfo)))
255 |
256 | (facts "and the value of @id is not a JSON-LD keyword, an absolute IRI, or a blank node identifier it is an invalid IRI mapping"
257 | (update-with-local-context (dissoc active-context "@vocab") {"foo" {"@id" "bar"}}) =>
258 | (throws clojure.lang.ExceptionInfo))
259 |
260 | (facts "and the value of @id is @context it is an invalid IRI mapping"
261 | (update-with-local-context active-context {"foo" {"@id" "@context"}}) =>
262 | (throws clojure.lang.ExceptionInfo)))
263 |
264 | ;; 14.x
265 | (facts "when term has an @id that is the term and it contains a colon"
266 |
267 | ;; 14.1
268 | (fact "and the term's prefix has a term definition in the local context"
269 | (update-with-local-context active-context {"a:bar" {"@id" "a:bar"}
270 | "a" {"@id" "http://foo.com/"}}) =>
271 | (-> active-context
272 | (assoc "a" {"@id" "http://foo.com/"})
273 | (assoc "a:bar" {"@id" "http://foo.com/bar"})))
274 |
275 | ;; 1 cyclic iri mapping
276 | ;; TODO not able to set one of these up yet, and pretty sure we aren't handling it properly
277 | ;; How is this triggered by spec test error 10
278 | ; (fact "and the term's prefix has a term definition in the local context"
279 | ; (update-with-local-context active-context {"a" {"@id" "a:a"}}) =>
280 | ; (throws clojure.lang.ExceptionInfo))
281 |
282 | ;; 14.2
283 | (fact "and the term's prefix has a term definition in active context"
284 | (let [active-context-with-term-definition (assoc active-context "foo" {"@id" "http://foo.com/"})]
285 | (update-with-local-context active-context-with-term-definition {"foo:bar" {"@id" "foo:bar"}}) =>
286 | (assoc active-context-with-term-definition "foo:bar" {"@id" "http://foo.com/bar"})))
287 |
288 | ;; 14.3
289 | (fact "and the term has no term definition in the active context"
290 | (update-with-local-context active-context {"foo:bar" {"@id" "foo:bar"}}) =>
291 | (assoc active-context "foo:bar" {"@id" "foo:bar"})))
292 |
293 | ;; 15
294 | (facts "when term has an @id that is the term and it doesn't contain a colon"
295 |
296 | (fact "and the term has a vocabulary mapping"
297 | (update-with-local-context active-context {"foo" {"@id" "foo"}}) =>
298 | (assoc active-context "foo" {"@id" "http://vocab.com/foo"}))
299 |
300 |
301 | (fact "and the term doesn't have a vocabulary mapping it is an invalid IRI mapping"
302 | (update-with-local-context (dissoc active-context "@vocab") {"foo" {"@id" "foo"}}) =>
303 | (throws clojure.lang.ExceptionInfo))))
304 |
305 |
306 | (facts "about @container values in a defined term"
307 |
308 | (facts "a term defined with a valid @container adds the term and the container mapping to the term definition in the active context"
309 | (doseq [container-value ["@list" "@set" "@index" "@language"]]
310 | (update-with-local-context active-context {"foo" {"@container" container-value}}) =>
311 | (assoc active-context "foo" {"@container" container-value "@id" "http://vocab.com/foo"})))
312 |
313 | (fact "a term defined with @container that is not @list, @set, @index or @language is an invalid container mapping"
314 | (doseq [container-value (concat not-strings ["foo" "@id"])]
315 | (update-with-local-context active-context {"foo" {"@container" container-value}}) =>
316 | (throws clojure.lang.ExceptionInfo))))
317 |
318 | (facts "about @language values in a defined term"
319 |
320 | (facts "a term defined with a valid @language adds the @language mapping to the term definition in the active context"
321 | (doseq [language-value [language another-language nil]]
322 | (update-with-local-context active-context {"foo" {"@language" language-value}}) =>
323 | (assoc active-context "foo" {
324 | "@language" (if language-value (s/lower-case language-value) nil)
325 | "@id" "http://vocab.com/foo"})))
326 |
327 | (facts "a term defined with a valid @language and a @type skips adding the @language mapping to the active context"
328 | (doseq [language-value [language another-language nil]]
329 | (update-with-local-context active-context {"foo" {"@type" "@id" "@language" language-value}}) =>
330 | (assoc active-context "foo" {"@type" "@id" "@id" "http://vocab.com/foo"})))
331 |
332 | (facts "a term defined with @language that is not a string or null"
333 | (doseq [language-value not-strings]
334 | (update-with-local-context active-context {"foo" {"@language" language-value}}) =>
335 | (throws clojure.lang.ExceptionInfo))))))
--------------------------------------------------------------------------------
/src/clj_json_ld/term_definition.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.term-definition
2 | "
3 | Implement the create term definition algorithm as defined in
4 | the [Create Term Definition section](http://www.w3.org/TR/json-ld-api/#create-term-definition).
5 |
6 | Term definitions are created by parsing the information in the given local context for the given term.
7 | "
8 | (:require [clojure.string :as s]
9 | [clojure.core.match :refer (match)]
10 | [defun :refer (defun defun-)]
11 | [clj-json-ld.json-ld :as json-ld]
12 | [clj-json-ld.json-ld-error :refer (json-ld-error)]
13 | [clj-json-ld.iri :refer (expand-iri blank-node-identifier? absolute-iri? compact-iri?)]))
14 |
15 | (defun- handle-type
16 | ;; 10) If value contains the key @type:
17 | ([active-context term value :guard #(contains? % "@type") local-context defined]
18 |
19 | ;; 10.1) Initialize type to the value associated with the @type key...
20 | (let [type (get value "@type")]
21 |
22 | ;; ... which must be a string. Otherwise, an invalid type mapping error has been detected and processing is
23 | ;; aborted.
24 | (if-not (string? type)
25 | (json-ld-error "invalid type mapping error"
26 | (str "@type of term " term " in the local context is not a string.")))
27 |
28 | ;; 10.2) Set type to the result of using the IRI Expansion algorithm, passing active context, type for value,
29 | ;; true for vocab, false for document relative, local context, and defined....
30 | (let [expanded-type (expand-iri active-context type {
31 | :vocab true
32 | :document-relative false
33 | :local-context local-context
34 | :defined defined})]
35 | ;; ...If the expanded type is neither @id, nor @vocab, nor an absolute IRI, an invalid type mapping
36 | ;; error has been detected and processing is aborted.
37 | (if-not (or (contains? #{"@id" "@vocab"} expanded-type) (absolute-iri? expanded-type))
38 | (json-ld-error "invalid type mapping" (str "@type of term " term " in the local context is not valid.")))
39 |
40 | ;; 10.3) Set the type mapping for definition to type.
41 | (let [term-definition (or (get active-context term) {})]
42 | (assoc active-context term (assoc term-definition "@type" expanded-type))))))
43 |
44 | ; active-context has no @type key, so do nothing
45 | ([active-context _ _ _ _]
46 | active-context))
47 |
48 | (defun- add-optional-container
49 | "Add the value of @container in the term to the term definition map, with the key @container"
50 | ([value :guard #(contains? % "@container") term-definition]
51 | (assoc term-definition "@container" (get value "@container")))
52 | ([_ term-definition] term-definition)) ; value doesn't contain @container, so do nothing
53 |
54 | (defun- handle-reverse
55 | ;; 11) If value contains the key @reverse:
56 | ([active-context term value :guard #(contains? % "@reverse") local-context defined]
57 |
58 | ;; 11.1) If value contains an @id member, an invalid reverse property error has been detected and processing
59 | ;; is aborted.
60 | (if (contains? value "@id") (json-ld-error "invalid reverse property" (str term " contains both @reverse and @id")))
61 |
62 | (let [reverse-value (get value "@reverse")]
63 |
64 | ;; 11.2) If the value associated with the @reverse key is not a string, an invalid IRI mapping error has been
65 | ;; detected and processing is aborted.
66 | (if-not (string? reverse-value)
67 | (json-ld-error "invalid IRI mapping" (str "The value of @reverse for term " term " is not a string.")))
68 |
69 | ;; 11.3) Otherwise, set the IRI mapping of definition to the result of using the IRI Expansion algorithm,
70 | ;; passing active context, the value associated with the @reverse key for value, true for vocab, false for
71 | ;; document relative, local context, and defined. ...
72 | (let [expanded-reverse (expand-iri active-context reverse-value {
73 | :vocab true
74 | :document-relative false
75 | :local-context local-context
76 | :defined defined})]
77 | ;; ... If the result is neither an absolute IRI nor a blank node identifier, i.e., it contains no colon (:),
78 | ;; an invalid IRI mapping error has been detected and processing is aborted.
79 | (if-not (or (absolute-iri? expanded-reverse) (blank-node-identifier? expanded-reverse))
80 | (json-ld-error "invalid IRI mapping error"
81 | (str "The value of @reverse for term " term " is not a absolute IRI or a blank node identifier.")))
82 |
83 | ;; 11.4) If value contains an @container member, ... if its value is neither @set, nor @index, nor null,
84 | ;; an invalid reverse property error has been detected (reverse properties only support set- and
85 | ;; index-containers) and processing is aborted.
86 | (if (contains? value "@container")
87 | (let [container-value (get value "@container")]
88 | (if-not (or (= container-value "@set") (= container-value "@index") (= container-value nil))
89 | (json-ld-error "invalid reverse property"
90 | (str "The value of @container for term " term " was not @set, @index or null.")))))
91 |
92 | ;; 11.4) If value contains an @container member, set the container mapping of definition to its value
93 | ;; 11.5) Set the reverse property flag of definition to true.
94 | ;; 11.6) Set the term definition of term in active context to definition and the value associated with
95 | ;; defined's key term to true and return.
96 | (let [term-definition (or (get active-context term) {})]
97 | (assoc active-context term
98 | (add-optional-container value
99 | (-> term-definition
100 | (assoc "@reverse" expanded-reverse)
101 | (assoc :reverse true))))))))
102 |
103 | ; active-context has no @reverse key, so do nothing
104 | ([active-context _ _ _ _] active-context))
105 |
106 | (defn- match-13?
107 | "
108 | Given a tuple of term and value, return true if this condition holds:
109 | 13) If value contains the key @id and its value does not equal term:
110 | "
111 | [term-value]
112 | (let [term (first term-value)
113 | value (last term-value)
114 | id? (contains? value "@id")
115 | id-value (get value "@id")]
116 | (and id? (not (= id-value term)))))
117 |
118 | (declare create-term-definition)
119 | (defun- handle-iri-mapping
120 | "Potential match for each mutually exclusive case of IRI mapping: 13, 14 and 15."
121 |
122 | ;; 13) If value contains the key @id and its value does not equal term:
123 | ([active-context term-value :guard match-13? local-context defined]
124 |
125 | ;; 13.1) If the value associated with the @id key is not a string, an invalid IRI mapping error has been
126 | ;; detected and processing is aborted.
127 | (let [term (first term-value)
128 | value (last term-value)
129 | id-value (get value "@id")]
130 | (if-not (string? id-value)
131 | (json-ld-error "invalid IRI mapping" (str "The value of @id for term " term " was not a string.")))
132 |
133 | ;; 13.2) Otherwise, set the IRI mapping of definition to the result of using the IRI Expansion algorithm,
134 | ;; passing active context, the value associated with the @id key for value, true for vocab, false for document
135 | ;; relative, local context, and defined. ...
136 | (let [iri-mapping (expand-iri active-context id-value {
137 | :vocab true
138 | :document-relative false
139 | :local-context local-context
140 | :defined defined})]
141 |
142 | ;; ... If the resulting (expanded) IRI mapping is neither a keyword, nor an absolute IRI,
143 | ;; nor a blank node identifier, an invalid IRI mapping error has been detected
144 | ;; and processing is aborted; ...
145 | (if-not (or
146 | (contains? json-ld/keywords iri-mapping)
147 | (absolute-iri? iri-mapping)
148 | (blank-node-identifier? iri-mapping))
149 | (json-ld-error "invalid IRI mapping"
150 | (str "The value of @id for term " term " was not a JSON-LD keyword, an absolute IRI, or a blank node identifier.")))
151 |
152 | ;; ... if it equals @context, an invalid keyword alias error has been detected and processing is aborted.
153 | (if (= iri-mapping "@context")
154 | (json-ld-error "invalid keyword alias" (str "The value of @id for term " term " cannot be @context.")))
155 |
156 | ;; set the IRI mapping of definition to the result of using the IRI Expansion algorithm
157 | (let [term-definition (or (get active-context term) {})]
158 | (assoc active-context term (assoc term-definition "@id" iri-mapping))))))
159 |
160 | ;; 14) Otherwise if the term contains a colon (:):
161 | ([active-context term-value :guard #(re-find #":" (first %)) local-context defined]
162 |
163 | (let [term (first term-value)
164 | term-parts (s/split term #":")
165 | term-prefix (first term-parts)
166 | term-suffix (last term-parts)
167 | updated-context ; either the updated active-context result of recursing or the original active context
168 | ;; 14.1 If term is a compact IRI with a prefix that is a key in local context...
169 | (if (and (compact-iri? term) (contains? local-context term-prefix))
170 | ;; ...a dependency has been found. Use this algorithm recursively passing active context, local context,
171 | ;; the prefix as term, and defined.
172 | ;; Also... 2) Set the value associated with defined's term key to false. This indicates that the term
173 | ;; definition is now being created but is not yet complete.
174 | (first (create-term-definition active-context local-context term-prefix (assoc defined term false)))
175 | active-context)]
176 |
177 | ;; 14.2 If term's prefix has a term definition in active context,...
178 | (if (contains? updated-context term-prefix)
179 | ;; ...set the IRI mapping of definition to the result of concatenating the value
180 | ;; associated with the prefix's IRI mapping and the term's suffix.
181 | (let [term-definition (or (get updated-context term) {})
182 | iri-mapping (str (get-in updated-context [term-prefix "@id"]) term-suffix)]
183 | (assoc updated-context term (assoc term-definition "@id" iri-mapping)))
184 | ;; 14.3 Otherwise, term is an absolute IRI or blank node identifier. Set the IRI mapping of definition to term.
185 | (let [term-definition (or (get updated-context term) {})]
186 | (assoc updated-context term (assoc term-definition "@id" term))))))
187 |
188 | ;; 15) Otherwise, ...
189 | ([active-context term-value _ _]
190 | ;; ... if active context has a vocabulary mapping, the IRI mapping of definition is set to the result of
191 | ;; concatenating the value associated with the vocabulary mapping and term. If it does not have a vocabulary
192 | ;; mapping, an invalid IRI mapping error been detected and processing is aborted.
193 | (if-let [vocabulary-mapping (get active-context "@vocab")]
194 | (let [term (first term-value)
195 | term-definition (or (get active-context term) {})]
196 | (assoc active-context term (assoc term-definition "@id" (str vocabulary-mapping term))))
197 | (json-ld-error "invalid IRI mapping" (str "There is no vocabulary mapping for the term " (first term-value))))))
198 |
199 | (defun- handle-container
200 | ;; 16) If value contains the key @container:
201 | ([active-context term value :guard #(contains? % "@container")]
202 |
203 | ;; 16.1) Initialize container to the value associated with the @container key, ...
204 | (let [container-value (get value "@container")]
205 | ;; ... which must be either @list, @set, @index, or @language. Otherwise, an
206 | ;; invalid container mapping error has been detected and processing is aborted.
207 | (if-not (contains? #{"@list" "@set" "@index" "@language"} container-value)
208 | (json-ld-error "invalid container mapping"
209 | (str "The value of @container for term " term " was not @list, @set, @index or @language.")))
210 |
211 | ;; 16.2) Set the container mapping of definition to container.
212 | (let [term-definition (or (get active-context term) {})]
213 | (assoc active-context term (assoc term-definition "@container" container-value)))))
214 |
215 | ; active-context has no @container key, so do nothing
216 | ([active-context _ _] active-context))
217 |
218 | (defun- handle-language
219 | ;; 17) If value contains the key @language and does not contain the key @type:
220 | ([active-context term value :guard #(and (contains? % "@language") (not (contains? % "@type")))]
221 |
222 | ;; 17.1) Initialize language to the value associated with the @language key, which must be either null or
223 | ;; a string. Otherwise, an invalid language mapping error has been detected and processing is aborted.
224 | (let [language (get value "@language")]
225 | (if-not (or (string? language) (nil? language))
226 | (json-ld-error "invalid language mapping" (str "The value of @language for term " term " was not a string or null.")))
227 |
228 | ;; 17.2) If language is a string set it to lowercased language.
229 | ;; Set the language mapping of definition to language.
230 | (let [term-definition (or (get active-context term) {})]
231 | (assoc active-context term
232 | (assoc term-definition "@language" (if language (s/lower-case language) nil))))))
233 |
234 | ; active-context has no @language key, so do nothing
235 | ([active-context _ _] active-context))
236 |
237 | (defn- new-term-definition
238 | "
239 | 9) Create a new term definition, definition.
240 | "
241 | [active-context local-context term defined]
242 |
243 | (let [value (get local-context term)
244 | updated-context
245 | (-> active-context
246 | ;; 10) If value contains the key @type:
247 | (handle-type term value local-context defined)
248 | ;; 11) If value contains the key @reverse:
249 | (handle-reverse term value local-context defined)
250 | ;; 13) - 14) - 15)
251 | (handle-iri-mapping [term value] local-context defined)
252 | ;; 16) If value contains the key @container:
253 | (handle-container term value)
254 | ;; 17) If value contains the key @language and does not contain the key @type:
255 | (handle-language term value))]
256 | ;; Return a tuple of the updated context and the defined map with the term marked as defined
257 | [updated-context (assoc defined term true)]))
258 |
259 | (defn create-term-definition
260 | "
261 | 6.2) Create Term Definition
262 |
263 | Algorithm called from the Context Processing algorithm to create a term definition in the
264 | active context for a term being processed in a local context.
265 | "
266 | [active-context local-context term defined]
267 |
268 | ;; 1) If defined contains the key term and the associated value is true
269 | ;; (indicating that the term definition has already been created), return.
270 | ;; Otherwise, if the value is false, a cyclic IRI mapping error has been detected
271 | ;; and processing is aborted.
272 | (let [defined-marker (get defined term)]
273 | (match [defined-marker] ; true, false or nil
274 |
275 | ; the term has already been created, so just return the result tuple
276 | [true] [active-context defined]
277 |
278 | ;; the term is in the process of being created, this is cyclical, oh noes!
279 | [false] (json-ld-error "cyclic IRI mapping"
280 | (str "local context has a term " term " that is used in its own definition"))
281 |
282 | ;; The term is not yet defined, so proceed to steps 2-18 of the algorithm
283 | [_] (do
284 |
285 | ;; 3) Since keywords cannot be overridden, the term must not be a keyword. Otherwise, a
286 | ;; keyword redefinition error has been detected and processing is aborted.
287 | (if (json-ld/keywords term)
288 | (json-ld-error "keyword redefinition"
289 | (str term " is a keyword and can't be used in a local context")))
290 |
291 | ;; 4) Remove any existing term definition for term in active context.
292 | ;; 5) Initialize value to a copy of the value associated with the key term in local context.
293 | (let [updated-context (dissoc active-context term)
294 | value (get local-context term)]
295 |
296 | (match [value]
297 | ;; 6) If value is null or value is a JSON object containing the key-value pair @id-null, set
298 | ;; the term definition in active context to null, set the value associated with defined's key
299 | ;; term to true, and return.
300 | [nil] [(assoc updated-context term nil) (assoc defined term true)]
301 | [_ :guard #(and
302 | (associative? %)
303 | (and
304 | (contains? % "@id")
305 | (= (get % "@id") nil)))]
306 | [(assoc updated-context term nil) (assoc defined term true)]
307 |
308 | ;; 7) Otherwise, if value is a string, convert it to a JSON object consisting of a
309 | ;; single member whose key is @id and whose value is value.
310 | [value :guard string?] [(assoc updated-context term {"@id" value}) (assoc defined term true)]
311 |
312 | ;; 8) Otherwise, value must be a JSON object...
313 | [_ :guard map?]
314 | ;; 9) Create a new term definition, definition.
315 | (new-term-definition active-context local-context term defined)
316 |
317 | ;; 8) ... if not, an invalid term definition
318 | ;; error has been detected and processing is aborted.
319 | :else (json-ld-error "invalid term definition"
320 | (str "The term " term " in the local context is not valid."))
321 | )
322 | )
323 |
324 | ;;18) Set the term definition of term in active context to definition and set the value
325 | ;; associated with defined's key term to true.
326 | )
327 | )
328 | )
329 | )
--------------------------------------------------------------------------------
/src/clj_json_ld/expand.clj:
--------------------------------------------------------------------------------
1 | (ns clj-json-ld.expand
2 | "
3 | Implement the Expansion operation as defined in the
4 | [Expansion Algorithm section](http://www.w3.org/TR/json-ld-api/#expansion-algorithm).
5 | "
6 | (:require [defun :refer (defun-)]
7 | [clojure.string :as s]
8 | [clj-json-ld.util.format :refer (ingest-input format-output)]
9 | [clj-json-ld.value :refer (expand-value)]
10 | [clj-json-ld.json-ld-error :refer (json-ld-error)]
11 | [clj-json-ld.context :refer (update-with-local-context)]
12 | [clj-json-ld.iri :refer (expand-iri)]
13 | [clj-json-ld.json-ld :refer (json-ld-keyword?)]
14 | [clj-json-ld.util.zipmap-filter :refer (zipmap-filter-values)]))
15 |
16 | (defn- drop-key?
17 | "
18 | 7.3) If expanded property is null or it neither contains a colon (:) nor it is a keyword, drop key by
19 | continuing to the next key."
20 | [key]
21 | (or
22 | (nil? key)
23 | (and
24 | (not (re-find #":" key))
25 | (not (json-ld-keyword? key)))))
26 |
27 | (defn- string-or-sequence-of-strings?
28 | "true if the value is either a string or a sequence containing only strings."
29 | [value]
30 | (or (string? value) (and (sequential? value) (every? string? value))))
31 |
32 | (defn- scalar? [element]
33 | (or (string? element) (number? element) (true? element) (false? element)))
34 |
35 | (defn- type-as-array [key values]
36 | (let [value (get values key)]
37 | (if (and (= key "@type") (not (sequential? value))) [value] value)))
38 |
39 | (defun- as-sequence
40 | "Fore a value to be sequential if it's not already, a nil value is an empty sequence."
41 | ([result :guard nil?] [])
42 | ([result :guard sequential?] result)
43 | ([result] [result]))
44 |
45 | (defun- nil-or-empty-value?
46 | (["@value" nil] true)
47 | (["@value" value :guard #(and (sequential? %) (empty? %))] true)
48 | ([_ _] false))
49 |
50 | ;; TODO replace with filter-map implementation
51 | (defn- filter-null-values
52 | "8.2) If the value of result's @value key is null, then set result to null."
53 | [k-v-map]
54 | (let [keys-to-remove (filter #(nil-or-empty-value? % (get k-v-map %)) (keys k-v-map))]
55 | (apply dissoc k-v-map keys-to-remove)))
56 |
57 | (defn- value-as-set-value
58 | "10.2) If result contains the key @set, then set result to the key's associated value."
59 | [key, values]
60 | (let [value (first (get values key))] ;; value is likely an array at this point
61 | (if (and (associative? value) (contains? value "@set"))
62 | (let [set-value (get values "@set")]
63 | (if (nil? set-value)
64 | [] ;; empty array as the value of the property
65 | [set-value])) ;; the value of this property is now the value of @set
66 | (get values key)))) ;; return the original value
67 |
68 | (defn- has-list-container-mapping?
69 | "True if the container mapping associated to key in active context is @list"
70 | ([active-context-tuple] (has-list-container-mapping? (first active-context-tuple) (last active-context-tuple)))
71 | ([key active-context] (= (get-in active-context [key "@container"]) "@list")))
72 |
73 | (defun- convert-to-list-object
74 | "7.9) If the container mapping associated to key in active context is @list and expanded value is not
75 | already a list object, convert expanded value to a list object by first setting it to an array
76 | containing only expanded value if it is not already an array, and then by setting it to a JSON object
77 | containing the key-value pair @list-expanded value."
78 | ;; have a list!
79 | ([_ expanded-value :guard #(and (associative? %) (contains? % "@list"))] expanded-value) ;; expanded value already IS a list object
80 | ([_ expanded-value :guard #(and (sequential? %) (associative? (first %)) (contains? (first %) "@list"))] expanded-value) ;; expanded value already IS a sequence containing a list object
81 | ;; need a list, don't have it
82 | ([active-context-tuple :guard has-list-container-mapping? expanded-value :guard sequential?] [{"@list" expanded-value}]) ;; the container mapping associated to key in active context is @list and expanded value is not already a list object, (so we need one), and it's already an array
83 | ([active-context-tuple :guard has-list-container-mapping? expanded-value] [{"@list" [expanded-value]}]) ;; the container mapping associated to key in active context is @list and expanded value is not already a list object (so we need one), and it's not an array
84 | ;; don't need a list
85 | ([_ expanded-value] expanded-value)) ;; there is no @list container mapping associated to key in active context
86 |
87 | (declare expansion)
88 | (declare expand-to-array)
89 | (defun- expand-property
90 |
91 | ;; 7.4.1) If active property equals @reverse, an invalid reverse property map error has been detected and
92 | ;; processing is aborted.
93 | ([active-context active-property :guard #(= % "@reverse") expanded-property-value :guard #(json-ld-keyword? (first %))]
94 | (json-ld-error "invalid reverse property map"
95 | (str "The active property is @reverse and " (first expanded-property-value) " is a JSON-LD keyword.")))
96 |
97 | ;; 7.4.3) If expanded property is @id and value is not a string, an invalid @id value error has been detected
98 | ;; and processing is aborted...
99 | ([active-context active-property expanded-property-value :guard #(and (= (first %) "@id") (not (string? (last %))))]
100 | (json-ld-error "invalid @id" (str "The value " (last expanded-property-value) " is not a valid @id.")))
101 | ;; ...Otherwise, set expanded value to the result of using the IRI Expansion algorithm, passing active context,
102 | ;; value, and true for document relative.
103 | ([active-context active-property expanded-property-value :guard #(= (first %) "@id")]
104 | (expand-iri active-context (last expanded-property-value) {:document-relative true}))
105 |
106 | ;; 7.4.4) If expanded property is @type and value is neither a string nor an array of strings, an invalid
107 | ;; @type value error has been detected and processing is aborted...
108 | ([active-context active-property
109 | expanded-property-value :guard #(and (= (first %) "@type") (not (string-or-sequence-of-strings? (last %))))]
110 | (json-ld-error "invalid @type value" (str "The value " (last expanded-property-value) " is not a valid @type.")))
111 | ;; ...Otherwise, set expanded value to the result of using the IRI Expansion algorithm, passing active context,
112 | ;; true for vocab, and true for document relative to expand the value or each of its items.
113 | ([active-context active-property expanded-property-value :guard #(= (first %) "@type")]
114 | (let [value (last expanded-property-value)]
115 | (if (sequential? value)
116 | (map #(expand-iri active-context % {:document-relative true :vocab true}) value)
117 | (expand-iri active-context value {:document-relative true :vocab true}))))
118 |
119 | ;; 7.4.6) If expanded property is @value and value is not a scalar or null, an invalid value object value error
120 | ;; has been detected and processing is aborted...
121 | ([active-context active-property expanded-property-value :guard #(and (= (first %) "@value") (not (or (scalar? (last %)) (nil? (last %)))))]
122 | (json-ld-error "invalid value object value" (str "The value " (last expanded-property-value) " is not a valid @value.")))
123 | ;; ... Otherwise, set expanded value to value. If expanded value is null, set the @value member of result to null and
124 | ;; continue with the next key from element.
125 | ;; Null values need to be preserved in this case as the meaning of an @type member depends on the existence of an @value member.
126 | ([active-context active-property expanded-property-value :guard #(= (first %) "@value")]
127 | (let [value (last expanded-property-value)]
128 | value))
129 |
130 | ;; 7.4.7) If expanded property is @language and value is not a string, an invalid language-tagged string error
131 | ;; has been detected and processing is aborted...
132 | ([active-context active-property expanded-property-value :guard #(and (= (first %) "@language") (not (string? (last %))))]
133 | (json-ld-error "invalid language-tagged string" (str "The value " (last expanded-property-value) " is not a valid @language string.")))
134 | ;; ...Otherwise, set expanded value to lowercased value.
135 | ([active-context active-property expanded-property-value :guard #(= (first %) "@language")]
136 | (s/lower-case (last expanded-property-value)))
137 |
138 | ;; 7.4.8) error
139 | ;; 7.4.11) error
140 |
141 | ;; 7.4.9) If expanded property is @list:
142 |
143 | ;; 7.4.9.1) If active property is null or @graph, continue with the next key from element to remove the free-floating list.
144 | ([active-context active-property :guard #(or (nil? %) (= % "@graph")) expanded-property-value :guard #(= (first %) "@list")]
145 | nil)
146 | ;; 7.4.9.2) Otherwise, initialize expanded value to the result of using this algorithm recursively passing active context,
147 | ;; active property, and value for element.
148 | ([active-context active-property expanded-property-value :guard #(= (first %) "@list")]
149 | ;; TODO
150 | ;; 7.4.9.3) If expanded value is a list object, a list of lists error has been detected and processing is aborted.
151 | (let [list-result (expand-to-array active-context active-property (last expanded-property-value))]
152 | list-result))
153 |
154 | ;; return nil for:
155 | ;; 7.4.9.1) If active property is null or @graph, continue with the next key from element to remove the
156 | ;; free-floating list.
157 | ;; -or-
158 | ;; ...Otherwise, set expanded value to the result of using the IRI Expansion algorithm,
159 | ;; passing active context, value, and true for document relative.
160 | ;; -or-
161 | ;; 7.4.5) If expanded property is @graph, set expanded value to the result of using this algorithm recursively
162 | ;; passing active context, @graph for active property, and value for element.
163 | ;; -or-
164 | ;; 7.4.9.2) Otherwise, initialize expanded value to the result of using this algorithm recursively passing active
165 | ;; context, active property, and value for element.
166 | ;; -or-
167 |
168 | ;; 7.4.10) If expanded property is @set, set expanded value to the result of using this algorithm recursively,
169 | ;; passing active context, active property, and value for element.
170 | ([active-context active-property expanded-property-value :guard #(= (first %) "@set")]
171 | (expand-to-array active-context active-property (last expanded-property-value)))
172 |
173 | ;; -or-
174 | ;; 7.4.11.1) Initialize expanded value to the result of using this algorithm recursively, passing active context,
175 | ;; @reverse as active property, and value as element.
176 |
177 | ;; 7.4.9.3) If expanded value is a list object, a list of lists error has been detected and processing is aborted.
178 |
179 | ;; 7.4.12) Unless expanded value is null, set the expanded property member of result to expanded value.
180 | ;; or not 7.4)
181 | ([active-context active-property expanded-property-value]
182 | ;; 7.5
183 | ;; or
184 | ;; 7.6
185 | ;; or
186 | ;; 7.7 Otherwise, initialize expanded value to the result of using this algorithm recursively, passing active context, key for active property, and value for element.
187 | (expand-to-array active-context (first expanded-property-value) (last expanded-property-value))
188 | ))
189 |
190 | (defun- expansion
191 |
192 | ;; 1) If element is null, return null.
193 | ([_ _ nil] nil)
194 |
195 | ;; 2) If element is a scalar,...
196 | ([active-context active-property element :guard #(scalar? %)]
197 |
198 | ;; 2.1) If active property is null or @graph,...
199 | (if (or (= active-property nil) (= active-property "@graph"))
200 | ;; ...drop the free-floating scalar by returning null.
201 | nil
202 | ;; 2.2) Return the result of the Value Expansion algorithm, passing the active context, active property,
203 | ;; and element as value.
204 | (expand-value active-context active-property element)))
205 |
206 | ;; 3) If element is an array, ...
207 | ([active-context active-property element :guard #(sequential? %)]
208 |
209 | ;; 3.1) Initialize an empty array, result.
210 | ;; 3.2) For each item in element:
211 | ;; 3.2.1) Initialize expanded item to the result of using this algorithm recursively, passing active context,
212 | ;; active property, and item as element.
213 | (let [expanded-elements (map #(expansion active-context active-property %) element)]
214 |
215 | ;; 3.2.2) If the active property is @list or its container mapping is set to @list, ...
216 | (if (or (= active-property "@list") (= (get active-property "@container") "@list"))
217 | (doseq [expanded-element expanded-elements]
218 | ;; ... the expanded item must not be an array or a list object,...
219 | (if (or (sequential? expanded-element) (contains? expanded-element "@list"))
220 | ;; ...otherwise a list of lists error has been detected and processing is aborted.
221 | (json-ld-error "list of lists" "A list of lists was encountered during expansion."))))
222 |
223 | ;; 3.2.3) If expanded item is an array, append each of its items to result. Otherwise, if expanded item is
224 | ;; not null, append it to result.
225 | ;; 3.3 Return result.
226 | (filter #(not (nil? %)) (flatten expanded-elements))))
227 |
228 | ;; 5) If element contains the key @context, set active context to the result of the Context Processing algorithm,
229 | ;; passing active context and the value of the @context key as local context.
230 | ([active-context active-property element :guard #(and (associative? %) (contains? % "@context"))]
231 | (expansion
232 | (update-with-local-context active-context (get element "@context"))
233 | active-property
234 | (dissoc element "@context")))
235 |
236 | ;; 4) Otherwise element is a JSON object.
237 | ([active-context active-property element]
238 |
239 | (let [
240 | ;; 7) For each key and value in element, ordered lexicographically by key:
241 | original-keys (sort (keys element))
242 |
243 | ;; TODO
244 | ;; 7.1) If key is @context, continue to the next key.
245 |
246 | ;; 7.2) Set expanded property to the result of using the IRI Expansion algorithm, passing active context,
247 | ;; key for value, and true for vocab.
248 | expanded-keys (map #(expand-iri active-context % {:vocab true}) original-keys)
249 | original-key-expanded-key-map (zipmap original-keys expanded-keys)
250 |
251 | expanded-values (map #(expand-property active-context % [% (get element %)]) original-keys)
252 | original-key-expanded-value-map (zipmap original-keys expanded-values)
253 | expanded-key-expanded-value-map (zipmap expanded-keys expanded-values)
254 |
255 | ;; 7.9) If the container mapping associated to key in active context is @list and expanded value is not
256 | ;; already a list object, convert expanded value to a list object by first setting it to an array
257 | ;; containing only expanded value if it is not already an array, and then by setting it to a JSON object
258 | ;; containing the key-value pair @list-expanded value.
259 | list-map (zipmap (map #(get original-key-expanded-key-map %) original-keys)
260 | (map #(convert-to-list-object [% active-context] (get original-key-expanded-value-map %)) original-keys))
261 |
262 | ;; 7.3) If expanded property is null or it neither contains a colon (:) nor it is a keyword, drop key by
263 | ;; continuing to the next key.
264 | keys-to-remove (filter drop-key? expanded-keys)
265 | k-v-map (apply dissoc list-map keys-to-remove)
266 |
267 | ;; 7.4.12) Unless expanded value is null, set the expanded property member of result to expanded value.
268 | ;; filter out null values
269 | ;; 7.8) If expanded value is null, ignore key by continuing to the next key from element.
270 | filtered-value-map (zipmap-filter-values #(not (nil? %)) (keys k-v-map) (vals k-v-map))
271 |
272 |
273 |
274 | ;; 7.10)
275 |
276 | ;; 7.11)
277 | ]
278 | ; (println "ok:" original-keys)
279 | ; (println "ek:" expanded-keys)
280 | ; (println "ev:" expanded-values)
281 | ; (println "okev:" original-key-expanded-value-map)
282 | ; (println "ekev:" expanded-key-expanded-value-map)
283 | ; (println "lm:" list-map)
284 | ; (println "kr:" keys-to-remove)
285 | ; (println "kv:" k-v-map)
286 | ; (println "fvm:" filtered-value-map)
287 |
288 | (if (empty? filtered-value-map) nil filtered-value-map)
289 | ;; 7.4.2) If result has already an expanded property member, an colliding keywords error has been detected
290 | ;; and processing is aborted.
291 | )))
292 |
293 | (defn- expand-to-array [active-context active-property element]
294 | (let [result (expansion active-context active-property element)]
295 | (if (nil? result) nil (as-sequence result))))
296 |
297 | (defn expand-it [input options]
298 | ;; TODO
299 | ;; If, after the above algorithm is run, the result is a JSON object that contains only an @graph key, set the result to the value of @graph's value.
300 | ;; TODO
301 | ;; Otherwise, if the result is null, set it to an empty array. Finally, if the result is not an array, then set the result to an array containing only the result.
302 | ;; Finally, if the result is not an array, then set the result to an array containing only the result.
303 | ;; TODO replace this series of lets with -> macro
304 | (let [result (expansion nil nil (ingest-input input options))
305 | ;; 8-13 detect some error conditions and tidy up the result
306 | ;; 8) TODO
307 | ;; 8.1) TODO
308 | ;; 8.2) If the value of result's @value key is null, then set result to null.
309 | filtered-result (filter-null-values result)
310 | ;; 9) Otherwise, if result contains the key @type and its associated value is not an array, set it to an array containing only the associated value.
311 | type-array-result (zipmap (keys filtered-result) (map #(type-as-array % filtered-result) (keys filtered-result)))
312 | ;; 10) Otherwise, if result contains the key @set or @list:
313 | ;; TODO 10.1) The result must contain at most one other key and that key must be @index.
314 | ;; Otherwise, an invalid set or list object error has been detected and processing is aborted.
315 | ;; 10.2) If result contains the key @set, then set result to the key's associated value.
316 | set-result (zipmap (keys type-array-result) (map #(value-as-set-value % type-array-result) (keys type-array-result)))
317 | ;; 11)
318 |
319 | ;; 12) If active property is null or @graph, drop free-floating values as follows:
320 | ;; 12.1) If result is an empty JSON object or contains the keys @value or @list, set result to null.
321 |
322 | ;; 12.2) Otherwise, if result is a JSON object whose only key is @id, set result to null.
323 | final-result (if (and (= (count set-result) 1) (contains? set-result "@id")) nil set-result)]
324 | ;; 13) Return result
325 | (format-output (as-sequence final-result) options)))
--------------------------------------------------------------------------------