├── .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

Define the JSON-LD keywords as specified in the Syntax Tokens and Keywords section.

json-ld-keyword?

(json-ld-keyword? value)

Returns true if the value is a JSON-LD keyword, otherwise false.

keywords

-------------------------------------------------------------------------------- /doc/API/index.html: -------------------------------------------------------------------------------- 1 | 2 | Clj-json-ld 0.1.0-SNAPSHOT API documentation

Clj-json-ld 0.1.0-SNAPSHOT

JSON-LD for Clojure

clj-json-ld.core

Public variables and functions:

clj-json-ld.json-ld

Define the JSON-LD keywords as specified in the Syntax Tokens and Keywords section.

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 | [![MPL License](http://img.shields.io/badge/license-MPL-blue.svg?style=flat)](https://www.mozilla.org/MPL/2.0/) 5 | [![Build Status](http://img.shields.io/travis/SnootyMonkey/clj-json-ld.svg?style=flat)](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 | [![Build Status](http://img.shields.io/travis/SnootyMonkey/clj-json-ld.svg?style=flat)](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))) --------------------------------------------------------------------------------