...
 
Commits (8)
......@@ -5,7 +5,7 @@
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "browserify -s n3 index.js -o ../../src/cljs_rdf/n3.js"
"build": "browserify -s n3 index.js -o ../../src/rdf/n3.js"
},
"author": "",
"license": "GPL-3.0-or-later",
......
;; Copyright © 2020 pukkamustard <[email protected]>
;;
;; This file is part of GeoPub.
;;
;; GeoPub is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; GeoPub is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GeoPub. If not, see <https://www.gnu.org/licenses/>.
(ns cljs-rdf.core)
(defmacro defns
"Create a new namespace"
[prefix namespace]
(let
[relative (gensym "relative-")]
`(defn ~prefix
[~relative]
(cljs-rdf.core/iri
(str ~namespace (name ~relative))))))
(ns cljs-rdf.graph.map
"A graph based on hash maps.
Two indexes are created. One mapping from subject to predicate to object and one from object to predicate to subject. Depending on query the better index is used. "
(:require [cljs-rdf.core :as rdf]
[cljs.core.logic :as l]))
;; Index helpers
(defn- assoc-clean
"If value is non-nil and non-empty replace value of key in map, else dissoc key."
[map k v]
(if (< 0 (count v))
(assoc map k v)
(dissoc map k)))
(defn- add-to-index
"add subject predicate object to the mapping from subject to predicate to object"
[map s p o]
(merge-with
;; if subject already has triple with same predicate
(fn [predicates new-predicate]
;; add object to the set of objects for the predicate
(merge-with (fn [object-set new-object]
(clojure.set/union object-set new-object))
predicates new-predicate))
map
(hash-map s (hash-map p (hash-set o)))))
(defn- delete-from-index
"delete from s-p-o mapping"
[map s p o]
;; replace subject to predicate mapping
(assoc-clean map s
;; with predicate to object set mapping
(assoc-clean (get map s) p
;; with object removed from object-set
(disj (get-in map [s p]) o))))
(defn- map-cons
"cons a to every element of c"
[c a]
(map
;; cons a to element of c and make sure element of c is a seq.
#(cons a (if (seq? %) % (list %)))
c))
;; Match protocol
(defprotocol IMatch
"Protocol for matching data to a pattern. Pattern may contain logical variables."
(-match [x pattern] "Match data to pattern and return sequence of matches."))
(extend-protocol IMatch
nil
(-match [index pattern]
'())
PersistentHashSet
(-match [index pattern]
;; pattern should not be a coll. if it is, take the first value from it.
(let [pattern (if (coll? pattern) (first pattern) pattern)]
(if (l/lvar? pattern)
(seq index)
(filter (partial = pattern) index))))
PersistentHashMap
(-match [index pattern]
(let [;; the value we match with at this level
a (first pattern)]
(if (not (l/lvar? a))
;; a is concrete
(->
;; get the sub-data we continue to match on
(get index a)
;; match for the rest of the pattern
(-match (next pattern))
;; add a to list of matches
(map-cons a))
;; a is a logical variable. Iterate over all keys in the map and recursively match on concrete values.
(mapcat #(-match index (cons % (next pattern)))
(keys index))))))
(defn- reverse-matches [matches]
(map reverse matches))
(defn- reify-match [match]
(let [s (first match)
p (second match)
o (nth match 2)]
(rdf/triple s p o)))
(defn reify-matches [matches]
(map reify-match matches))
;; Graph
(declare ->Graph)
(defrecord Graph [spo ops]
rdf/IGraph
(rdf/graph-add [graph triple]
(let [s (rdf/triple-subject triple)
p (rdf/triple-predicate triple)
o (rdf/triple-object triple)]
(->Graph
(add-to-index (:spo graph) s p o)
(add-to-index (:ops graph) o p s))))
(rdf/graph-delete [graph triple]
(let [s (rdf/triple-subject triple)
p (rdf/triple-predicate triple)
o (rdf/triple-object triple)]
(->Graph
(delete-from-index (:spo graph) s p o)
(delete-from-index (:ops graph) o p s))))
(rdf/graph-has [graph triple]
(let [s (rdf/triple-subject triple)
p (rdf/triple-predicate triple)
o (rdf/triple-object triple)]
(some?
(get-in (:spo graph) [s p o]))))
(rdf/graph-match [graph triple]
(let [s (rdf/triple-subject triple)
p (rdf/triple-predicate triple)
o (rdf/triple-object triple)]
(cond
;; if o is concrete use the ops index
(not (l/lvar? o))
(-> (:ops graph)
(-match [o p s])
;; don't forget to reverse the matches
(reverse-matches)
(reify-matches))
;; else use the spo index
:else
(-> (:spo graph)
(-match [s p o])
(reify-matches)))))
(rdf/graph-seq [graph]
(rdf/graph-match graph (rdf/triple (l/lvar) (l/lvar) (l/lvar))))
(rdf/graph-tripleo [graph q]
(fn [a]
(let [q (l/-walk* a q)]
(cond
(rdf/triple? q)
(l/to-stream
(map #(l/unify a % q)
(rdf/graph-match graph q)))
;; dump all triples
(l/lvar? q)
(l/to-stream (map #(l/unify a % q)
(rdf/graph-match graph (rdf/triple (l/lvar) (l/lvar) (l/lvar)))))
;; will not match with anything that is not a string
:else (l/to-stream '()))))))
(defn graph [] (->Graph (hash-map) (hash-map)))
;; (-> (graph)
;; (rdf/graph-add (rdf/triple :a :b :c))
;; (rdf/graph-add (rdf/triple :a :b :d))
;; (rdf/graph-match (rdf/triple (l/lvar) (l/lvar) :d))
;; )
(ns cljs-rdf.graph.set
"Implement RDF Graph using PersistentHashSet"
(:require [cljs-rdf.core :as rdf]
[cljs.core.logic :as l]))
(extend-type PersistentHashSet
rdf/IGraph
(rdf/graph-add [graph triple] (conj graph triple))
(rdf/graph-delete [graph triple] (disj graph triple))
(rdf/graph-has [graph triple] (contains? graph triple))
(rdf/graph-match [graph triple]
;; TODO nested matching. Currently it is not possible to get all triples that have a BlankNode as subject (while not specifying the id of the BlankNode)
(let [s (rdf/triple-subject triple)
p (rdf/triple-predicate triple)
o (rdf/triple-object triple)]
(filter
(fn [tg]
(let [sg (rdf/triple-subject tg)
pg (rdf/triple-predicate tg)
og (rdf/triple-object tg)]
(and
(or (l/lvar? s) (= s sg))
(or (l/lvar? p) (= p pg))
(or (l/lvar? o) (= o og)))))
graph)))
(rdf/graph-tripleo [graph q]
(fn [a]
(let [q (l/-walk* a q)]
(cond
(rdf/triple? q)
(l/to-stream
(map #(l/unify a % q)
(rdf/graph-match graph q)))
;; dump all triples
(l/lvar? q)
(l/to-stream (map #(l/unify a % q) graph))
;; will not match with anything that is not a string
:else (l/to-stream '()))))))
(defn graph [] (hash-set))
;; Copyright © 2020 pukkamustard <[email protected]>
;;
;; This file is part of GeoPub.
;;
;; GeoPub is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; GeoPub is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GeoPub. If not, see <https://www.gnu.org/licenses/>.
(ns cljs-rdf.n3
"Bindings to the N3.js library"
(:require [cljs-rdf.core :as rdf]
["./n3.js" :as n3]))
;; Extend N3 classes to protocols
(def n3-quad-type
(.-Quad (.-internal (.-DataFactory n3))))
(extend-type n3-quad-type
rdf/ITriple
(rdf/triple-subject [x] (.-subject x))
(rdf/triple-predicate [x] (.-predicate x))
(rdf/triple-object [x] (.-object x))
IPrintWithWriter
(-pr-writer [o w _]
(write-all w
"#Quad {"
"subject: " (prn-str (rdf/triple-subject o))
", predicate: " (prn-str (rdf/triple-predicate o))
", object: " (prn-str (rdf/triple-object o))
"}"
)))
(def n3-named-node-type (.-NamedNode (.-internal (.-DataFactory n3))))
(extend-type n3-named-node-type
rdf/IIRI
(rdf/iri-value [x] (.-value x))
IPrintWithWriter
(-pr-writer [o w _] (write-all w "<" (rdf/iri-value o) ">")))
(def n3-blank-node-type (.-BlankNode (.-internal (.-DataFactory n3))))
(extend-type n3-blank-node-type
rdf/IBlankNode
(rdf/blank-node-id [x] (.-value x))
IPrintWithWriter
(-pr-writer [o w _] (write-all w "_:" (prn-str (rdf/blank-node-id o))))
)
(def n3-literal-type (.-Literal (.-internal (.-DataFactory n3))))
(extend-type n3-literal-type
rdf/ILiteral
(rdf/literal-value [x] (.-value x))
(rdf/literal-language [x] (.-language x))
(rdf/literal-datatype [x] (.-datatype x))
IPrintWithWriter
;; TODO also print datatype and language
(-pr-writer [o w _] (write-all w "\"" (pr-str (rdf/literal-value o)) "\"")))
;; Turtle parser
(defn parse [input]
"Parse an RDF document to Triples"
(map rdf/triple (js->clj (.parse (new n3/Parser) input))))
(ns cljs-rdf.turtle
(:require [cljs-rdf.core :as rdf]))
(defn- base-directive [base]
"")
(defn- prefix-directives [prefixes]
"")
(defn- serialize-iri [iri]
(str "<" (rdf/iri-value iri) ">"))
(defn- serialize-literal [literal]
(str "\"" (rdf/literal-value literal) "\""
(if (not-empty (rdf/literal-language literal))
(str "@" (rdf/literal-language literal)))
(if (rdf/literal-datatype literal)
(str "^^" (serialize-iri (rdf/literal-datatype literal)))))
)
(defn- serialize-object [obj]
(cond
(rdf/iri? obj) (serialize-iri obj)
(rdf/blank-node? obj) (str "_:" (rdf/blank-node-id obj))
(rdf/literal? obj) (serialize-literal obj)))
(defn- triple-statement [triple]
(let [s (rdf/triple-subject triple)
p (rdf/triple-predicate triple)
o (rdf/triple-object triple)]
(str (serialize-object s) " "
(serialize-object p) " "
(serialize-object o) " .")))
(defn- statements [triples base prefix]
(clojure.string/join "\n" (map triple-statement triples)))
(defn encode
"Encode a sequence of triples as RDF Turtle"
[triples & {:keys [base prefixes]}]
(statements triples base prefixes))
......@@ -28,8 +28,8 @@
[geopub.ui.timeline]
[geopub.cpub.core :as cpub]
[geopub.data.activitypub :as activitypub]
[cljs-rdf.core :as rdf]
[cljs-rdf.graph.map]
[rdf.core :as rdf]
[rdf.graph.map]
[reitit.core :as rc]
[reitit.frontend :as rf]
[reitit.frontend.easy :as rfe]))
......@@ -49,7 +49,7 @@
;; ============== State and helpers ==============
(defonce state (r/atom {:store (cljs-rdf.graph.map/graph)}))
(defonce state (r/atom {:store (rdf.graph.map/graph)}))
(defn state-store [state]
"Return the datastore"
......@@ -68,7 +68,7 @@
(defn reset-store []
"Helper to reset the store"
(swap! state #(assoc % :store (cljs-rdf.graph.map/graph))))
(swap! state #(assoc % :store (rdf.graph.map/graph))))
;; ============== Start fetching data ============
......@@ -153,7 +153,7 @@
;; how to like something:
;; (-> (activitypub/like (rdf/iri "http://openengiadina.net/"))
;; (cpub/post-rdf (str actor-id "/outbox") auth))
;; (cpub/post-rdf (str actor-id "/outbox") auth))
;; (reset-store)
;; (cpub-get-data!)
......
......@@ -18,10 +18,10 @@
(ns geopub.cpub.core
(:require-macros [cljs.core.async.macros :refer [go]])
(:require [cljs-http.client :as http]
[cljs.core.async :refer [<! poll!]]
[cljs-rdf.n3 :as n3]
[cljs-rdf.core :as rdf]
[cljs-rdf.turtle :as turtle]
[cljs.core.async :refer [<!]]
[rdf.n3 :as n3]
[rdf.core :as rdf]
;; [cljs-rdf.turtle :as turtle]
[clojure.pprint :refer [pprint]]))
(defn get-activitystreams-ontology []
......@@ -54,8 +54,10 @@
(defn post-rdf [data url auth]
(http/post url
{:with-credentials? false
:basic-auth auth
:headers {"Content-type" "text/turtle"}
:body (turtle/encode (rdf/graph-seq data))}))
(go
(let [body (<! (n3/encode data))]
(http/post url
{:with-credentials? false
:basic-auth auth
:headers {"Content-type" "text/turtle"}
:body body}))))
(ns geopub.data.activitypub
"Helpers to deal with ActivityPub data"
(:require-macros [cljs.core.logic :refer [run* fresh]])
(:require [cljs-rdf.core :as rdf]
[cljs-rdf.graph.map :as rdf-graph]
(:require [rdf.core :as rdf]
[rdf.graph.map :as rdf-graph]
[rdf.logic :as rl]
[rdf.description :as rd]
[rdf.ns :as ns]
[cljs.core.logic :as l]
[geopub.ns :refer [as rdfs]]))
......@@ -10,11 +13,11 @@
"Returns an activty"
[graph id]
;; Create description pointing to the activity
(rdf/description id
(rd/description id
;; with a graph
(reduce rdf/graph-add (cljs-rdf.graph.map/graph)
(reduce rdf/graph-add (rdf-graph/graph)
;; containing triples of the activity (three levels deep)
(run* [t] (rdf/collecto graph 3 id t)))))
(run* [t] (rl/collecto graph 3 id t)))))
(defn get-activities
"Returns all activities as a list of RDF descriptions"
......@@ -25,13 +28,13 @@
(fresh [activity-type]
;; Get all possible activity types
(rdf/graph-tripleo graph
(rl/graph-tripleo graph
(rdf/triple activity-type
(rdfs "subClassOf")
(as "Activity")))
;; Get all activities
(rdf/graph-typeo graph id activity-type))
(rl/graph-typeo graph id activity-type))
)]
(map (partial get-activity graph) activity-ids)))
......@@ -41,5 +44,5 @@
"Returns an activity to like an object"
[object]
(-> (rdf-graph/graph)
(rdf/graph-add (rdf/triple (rdf/iri "") (rdf/rdf :type) (as "Like")))
(rdf/graph-add (rdf/triple (rdf/iri "") (ns/rdf :type) (as "Like")))
(rdf/graph-add (rdf/triple (rdf/iri "") (as :object) object))))
......@@ -16,7 +16,7 @@
;; along with GeoPub. If not, see <https://www.gnu.org/licenses/>.
(ns geopub.ns
(:require-macros [cljs-rdf.core :refer [defns]]))
(:require-macros [rdf.core :refer [defns]]))
;; Commonly used namespaces
......
......@@ -5,7 +5,9 @@
[leaflet :as leaflet]
[react-leaflet :as react-leaflet]
[geopub.ns :refer [geo]]
[cljs-rdf.core :as rdf]
[rdf.core :as rdf]
[rdf.logic :as rl]
[rdf.description :as rd]
[cljs.core.logic :as l]))
......@@ -29,21 +31,21 @@
(defn get-geo-object [state]
"Returns a list of activities that have a latitude and longitude"
;; TODO only store the relevant subgraph in the description
(map #(rdf/description % (:store @state))
(map #(rd/description % (:store @state))
(run* [s]
(fresh [x y]
(rdf/graph-tripleo (:store @state) (rdf/triple s (geo "long") x))
(rdf/graph-tripleo (:store @state) (rdf/triple s (geo "lat") y))))))
(rl/graph-tripleo (:store @state) (rdf/triple s (geo "long") x))
(rl/graph-tripleo (:store @state) (rdf/triple s (geo "lat") y))))))
(defn get-location [object]
(let
[lat (-> object
(rdf/description-get (geo "lat"))
(rd/description-get (geo "lat"))
(first)
(rdf/literal-value))
long (-> object
(rdf/description-get (geo "long"))
(rd/description-get (geo "long"))
(first)
(rdf/literal-value))]
[lat long]))
......@@ -66,7 +68,7 @@
:attribution copy-osm}]
(for [geo-object (get-geo-object state)]
^{:key (prn-str (rdf/description-subject geo-object))}
^{:key (prn-str (rd/description-subject geo-object))}
[geo-object-component geo-object])
]])
(ns geopub.ui.store
(:require [cljs-rdf.core :as rdf]
[cljs.core.logic :as l]
(:require [rdf.core :as rdf]
[clojure.core.logic :as l]
[geopub.ui.utils :refer [rdf-term-component]]))
;; TODO this should probably go in Graph protocol
(defn all-triples [graph]
(rdf/graph-match graph (rdf/triple (l/lvar) (l/lvar) (l/lvar))))
(defn triple-table [graph]
(defn triple-table [triples]
[:table
[:tbody
(for [t (all-triples graph)]
(for [t triples]
^{:key (prn-str t)}
[:tr
[:td [rdf-term-component (rdf/triple-subject t)]]
......@@ -21,5 +17,6 @@
(defn view [state]
[:div#store
[:h1 "Store"]
[triple-table (:store @state)]
[triple-table (rdf/triple-seq
(:store @state))]
])
......@@ -19,17 +19,19 @@
(:require [geopub.ns :refer [as rdfs schema]]
[geopub.ui.utils :refer [iri-component literal-component]]
[geopub.data.activitypub :as activitypub]
[cljs-rdf.core :as rdf]
[cljs-rdf.graph.map]))
[rdf.core :as rdf]
[rdf.description :as rd]
[rdf.ns :as ns]
[rdf.graph.map]))
(defmulti object-component
(fn [object] (first (rdf/description-get object (rdf/rdf :type)))))
(fn [object] (first (rd/description-get object (ns/rdf :type)))))
(defmethod object-component
(as :Note)
[object]
[:div.object
(for [content (rdf/description-get object (as :content))]
(for [content (rd/description-get object (as :content))]
[:p [literal-component content]])])
(defmethod object-component
......@@ -48,20 +50,20 @@
;; render object
(for
[object
(map (partial rdf/description-move activity)
(rdf/description-get activity (as :object)))]
(map (partial rd/description-move activity)
(rd/description-get activity (as :object)))]
^{:key (prn-str (rdf/description-subject object))}
^{:key (prn-str (rd/description-subject object))}
[object-component object])
[:div.meta
[iri-component (rdf/description-get activity (as :actor))]
[iri-component (rd/description-get activity (as :actor))]
[:br]
[iri-component (rdf/description-get activity (rdf/rdf :type))]]])
[iri-component (rd/description-get activity (ns/rdf :type))]]])
(defn view [state]
[:div#timeline
[:h1 "Timeline"]
(for [activity (activitypub/get-activities (:store @state))]
^{:key (prn-str (rdf/description-subject activity))}
^{:key (prn-str (rd/description-subject activity))}
[activity-component activity])])
(ns geopub.ui.utils
(:require [cljs-rdf.core :as rdf]))
(:require [rdf.core :as rdf]))
(defn iri-component [iri & {:keys [class]}]
(let [class (or class "iri")]
......
#+TITLE: Clojure RDF
I intend to extract this part of GeoPub into seperate libraries:
- cljc-rdf: base protocols and data types (Clojure and ClojureScript)
- cljs-rdf-n3: bindings to N3.js
- cljs-rdf-parse: bidnings to rdf-parse.js
And then potentially there could be other bindings to Java libs (e.g. clj-rdf-jena)
Much inspiration is taken from following projects:
- [[https://github.com/marcelotto/rdf-ex][rdf-ex]]: RDF library for Elixir
- [[https://github.com/thi-ng/trio/][thi.ng/trio]]: Non-RDF triple store
- [[https://github.com/structureddynamics/clj-turtle][clj-turtle]]: A Clojure DSL to produce RDF/Turtle data.
- [[https://github.com/cordawyn/schemantic-web][schemantic-web]]: Semantic Web library for Scheme48.
- [[https://github.com/antoniogarrote/clj-plaza][clj-plaza]]
* License
The Clojure/ClojureScipt libraries should eventually be released under the EPL
(Eclipse Public License). It is the most common license in the Clojure world and
seems to be a bit like LGPL.
Contributors please be aware.
(ns rdf.core
"RDF in Clojure")
(declare iri)
(defmacro defns
"Macro for defining a custom new namespace"
[prefix namespace]
(let
[relative (gensym "relative-")]
`(defn ~prefix
[~relative]
(iri
(str ~namespace (name ~relative))))))
;; IRI
(defprotocol IIRI
(iri-value [x] "Returns the IRI value"))
(defprotocol IIRIConvert
"Protocol for data that can be converted to an iri."
(-as-iri [x] "Return value as an iri."))
(defrecord IRI [value]
IIRI
(iri-value [x] (:value x)))
(defn iri? [x] (instance? IRI x))
(defn iri
"Returns an iri"
[v]
(cond
;; value is already an iri
(instance? IRI v) v
;; value can be converted to an iri
(satisfies? IIRIConvert v) (-as-iri v)
;; else just pack in an IRI
:else (->IRI v)))
;; Literal
(defprotocol ILiteral
(literal-value [x] "Returns the literal value")
(literal-language [x] "Returns the literal language")
(literal-datatype [x] "Returns the literal datatype"))
(defprotocol ILiteralConvert
(-as-literal [x] "Converts value to Literal"))
(defrecord Literal [value language datatype]
ILiteral
(literal-value [x] (:value x))
(literal-language [x] (:language x))
(literal-datatype [x] (:datatype x)))
(defn literal? [x] (instance? Literal x))
(defn literal
"Returns a literal with optional language and datatype."
([value & {:keys [language datatype]}]
(cond
;; already a literal, nothing to do
(instance? Literal value) value
;; value can be converted to a literal
(satisfies? ILiteralConvert value) (-as-literal value)
;; return a new Literal
:else (->Literal value language datatype))))
;; Blank Node
(defprotocol IBlankNode
(blank-node-id [x] "Returns the blank node id"))
(defprotocol IBlankNodeConvert
(-as-blank-node [x] "Converts value to BlankNode"))
(defrecord BlankNode [id]
IBlankNode
(blank-node-id [x] (:id x)))
(defn blank-node? [x]
(instance? BlankNode x))
(defn blank-node
"Returns a blank node. If no id is suplied a new (and unique) id will be generated."
([] (->BlankNode (gensym)))
([v]
(cond
(instance? BlankNode v) v
(satisfies? IBlankNodeConvert v) (-as-blank-node v)
:else (->BlankNode v))))
;; Triple
(defprotocol ITriple
(triple-subject [x] "Returns the triple subject")
(triple-predicate [x] "Returns the triple predicate")
(triple-object [x] "Returns the triple object"))
(defprotocol ITripleConvert
(-as-triple [x] "Converts value to Triple"))
(defrecord Triple [subject predicate object]
ITriple
(triple-subject [x] (:subject x))
(triple-predicate [x] (:predicate x))
(triple-object [x] (:object x)))
(defn triple? [t]
(instance? Triple t))
(defn- iri-like? [s]
(or (iri? s) (satisfies? IIRIConvert s)))
(defn- blank-node-like? [s]
(or (blank-node? s) (satisfies? IBlankNodeConvert s)))
(defn- literal-like? [s]
(or (literal? s) (satisfies? ILiteralConvert s)))
(defn triple
"Returns a Triple"
([t] (cond
(instance? Triple t) t
(satisfies? ITripleConvert t) (-as-triple t)))
([s p o] (->Triple
;; attempt to cast subject
(cond
;; to IRI
(iri-like? s) (iri s)
;; or BlankNode
(blank-node-like? s) (blank-node s)
;; else just use as is
:else s)
;; attempt to cast predicate
(cond
;; to IRI
(iri-like? p) (iri p)
;; else just use as is
:else p)
;; attempt to cast object
(cond
;; to IRI
(iri-like? o) (iri o)
;; or BlankNode
(blank-node-like? o) (blank-node o)
;; or Literal
(literal-like? o) (literal o)
;; else just use as is
:else o))))
;; TODO quads
;; Protocols
(defprotocol IGraph
"Protocol for accessing a graph"
(graph-match [x triple] "Returns sequence of triples matching query."))
(defn graph? [x]
(satisfies? IGraph x))
(defprotocol IGraphUpdate
"Protocol for updating graph"
(graph-add [x triple] "Add a triple to the graph.")
(graph-delete [x triple] "Delete a triple from the graph."))
(defprotocol ITripleSeq
"Protocol for converting anything to a sequence of triples"
(triple-seq [x]))
(ns rdf.description
"A graph with a starting point.
Having a starting point allows lookup of like for a map.
"
(:require-macros [clojure.core.logic :refer [run*]])
(:require [rdf.core :as rdf]
[rdf.logic :as rl]
[clojure.core.logic :as l]))
(defrecord Description [subject graph])
(defn description [subject graph]
(->Description subject graph))
(defn description-subject [description]
(:subject description))
(defn description-move [description new-subject]
(->Description new-subject (:graph description)))
(defn description-get [description key]
(run* [o]
(rl/graph-tripleo (:graph description)
(rdf/triple (:subject description) key o))))
(ns rdf.graph.map
"A in-memory graph based on hash maps.
Two indexes are created. One mapping from subject to predicate to object and one from object to predicate to subject. Depending on query the better index is used."
(:require [rdf.core :as rdf]
[rdf.graph.map.index :refer [index-match index-merge index-remove]]
[clojure.core.logic :as l]))
;; helpers
(defn- reverse-matches [matches]
(map reverse matches))
(defn- reify-match [match]
(let [s (first match)
p (second match)
o (nth match 2)]
(rdf/triple s p o)))
(defn reify-matches [matches]
(map reify-match matches))
(defn- add-to-index [index a b c]
(index-merge index
(hash-map a (hash-map b (hash-set c)))))
(declare ->Graph)
(defrecord Graph [spo ops]
rdf/IGraph
(rdf/graph-match [graph triple]
(let [s (rdf/triple-subject triple)
p (rdf/triple-predicate triple)
o (rdf/triple-object triple)]
(cond
;; if o is concrete use the ops index
(not (l/lvar? o))
(-> (:ops graph)
(index-match [o p s])
;; don't forget to reverse the matches
(reverse-matches)
(reify-matches))
;; else use the spo index
:else
(-> (:spo graph)
(index-match [s p o])
(reify-matches)))))
rdf/IGraphUpdate
(rdf/graph-add [graph triple]
(let [s (rdf/triple-subject triple)
p (rdf/triple-predicate triple)
o (rdf/triple-object triple)]
(->Graph
(add-to-index (:spo graph) s p o)
(add-to-index (:ops graph) o p s))))
(rdf/graph-delete [graph triple]
(let [s (rdf/triple-subject triple)
p (rdf/triple-predicate triple)
o (rdf/triple-object triple)]
(->Graph
(index-remove (:spo graph) [s p o])
(index-remove (:ops graph) [o p s]))))
rdf/ITripleSeq
(rdf/triple-seq [graph]
(rdf/graph-match graph
(rdf/triple (l/lvar) (l/lvar) (l/lvar)))))
(defn graph [] (->Graph (hash-map) (hash-map)))
(ns rdf.graph.map.index
"in-memory index"
(:require [clojure.core.logic :as l]
[clojure.set :as set]))
;; helpers
(defn- assoc-clean
"If value is non-nil and non-empty replace value of key in map, else dissoc key."
[map k v]
(if (empty? v)
(dissoc map k)
(assoc map k v)
))
(defn- map-cons
"cons a to every element of c"
[c a]
(map
;; cons a to element of c and make sure element of c is a seq.
#(cons a (if (seq? %) % (list %)))
c))
(defprotocol IIndex
(index-merge [x y])
(index-remove [x path])
(index-match [x path]))
(extend-protocol IIndex
nil
(index-merge [_ _] nil)
(index-remove [_ _] nil)
(index-match [_ _] '())
PersistentHashSet
(index-merge [x y] (set/union x y))
(index-remove [x path] (disj x (first path)))
(index-match [index path]
(let [k (first path)]
(map list
(if (l/lvar? k)
(seq index)
(filter (partial = k) index)))))
PersistentHashMap
(index-merge [x y] (merge-with index-merge x y))
(index-remove [x path]
(let [k (first path)
rest (rest path)]
(assoc-clean x k (index-remove (get x k) rest))))
(index-match [index path]
(let [k (first path)]
(if (not (l/lvar? k))
;; k is concrete
(->
;; get the sub-data we continue to match on
(get index k)
;; match for the rest of the pattern
(index-match (rest path))
;; add a to list of matches
(map-cons k))
;; a is a logical variable. Iterate over all keys in the map and recursively match on concrete values.
(mapcat #(index-match index (cons % (rest path)))
(keys index))))))
;; Copyright © 2020 pukkamustard <[email protected]>
;;
;; This file is part of GeoPub.
;;
;; GeoPub is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; GeoPub is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GeoPub. If not, see <https://www.gnu.org/licenses/>.
(ns rdf.logic
"Use core.logic with RDF!"
(:require-macros [clojure.core.logic :refer [run* fresh]])
(:require [rdf.core :as rdf]
[rdf.ns :as ns]
[clojure.core.logic :as l]))
(ns cljs-rdf.core
"RDF in ClojureScript"
(:require-macros [cljs-rdf.core :refer [defns]]
[cljs-rdf.core :as rdf]
[cljs.core.logic :refer [run* fresh]])
(:require [cljs.core.logic :as l]))
;; Commonly used namespaces
(defns rdf "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
(defns xsd "http://www.w3.org/2001/XMLSchema#")
;; Basic Interfaces
(defprotocol ITriple
(triple-subject [x])
(triple-predicate [x])
(triple-object [x]))
(defn triple? [x]
(satisfies? ITriple x))
(defprotocol IIRI
(iri-value [x]))
(defn iri? [x]
(satisfies? IIRI x))
(defprotocol IBlankNode
(blank-node-id [x]))
(defn blank-node? [x]
(satisfies? IBlankNode x))
(defprotocol ILiteral
(literal-value [x])
(literal-language [x])
(literal-datatype [x]))
(defn literal? [x]
(satisfies? ILiteral x))
;; Graph
(defprotocol IGraph
(graph-add [x triple] "Add a triple to the dataset.")
(graph-delete [x triple] "Remove triple from the dataset.")
(graph-has [x triple] "Returns true if triple is in dataset, false if not.")
(graph-match [x q] "Returns sequence of triples matching query.")
(graph-seq [x] "Returns sequence of triples in the graph.")
(graph-tripleo [x q] "Unify graph into a relational program."))
(defn graph? [x]
(satisfies? IGraph x))
;; ;; Implement protocol for basic types
(extend-type js/String
IIRI
(iri-value [x] x)
ILiteral
(literal-value [x] (.valueOf x))
(literal-language [x] nil)
(literal-datatype [x] (xsd "string")))
(extend-type js/Number
ILiteral
(literal-value [x] (.valueOf x))
(literal-language [x] nil)
(literal-datatype [x]
(do
(cond
(integer? (.valueOf x)) (xsd "integer")
(double? (.valueOf x)) (xsd "double")))))
;; Implementation of data model using records
(declare ->IRI)
(defrecord IRI [value]
IIRI
(iri-value [v] (:value v))
;; TODO: program against the ILiteral, IIRI, etc. protocols and not the types!
(extend-type rdf/IRI
l/IUnifyTerms
(l/-unify-terms [u v s]
(if (instance? IRI v)
(if (instance? rdf/IRI v)
(l/unify s (:value u) (:value v))
(l/unify s v u)))
l/IWalkTerm
(l/-walk-term [v s]
(->IRI
(rdf/->IRI
(l/-walk* s (:value v))))
l/IReifyTerm
(l/-reify-term [v s]
(l/-reify* s (:value v))))
(defn iri [v]
(cond
(instance? IRI v) v
(iri? v) (->IRI (str (iri-value v)))
:else (->IRI v)))
(declare ->BlankNode)
(defrecord BlankNode [id]
IBlankNode
(blank-node-id [b] (:id b))
(extend-type rdf/BlankNode
l/IUnifyTerms
(l/-unify-terms [u v s]
(if (instance? BlankNode v)
(if (instance? rdf/BlankNode v)
(l/unify s (:id u) (:id v))
(l/unify s v u)))
l/IWalkTerm
(l/-walk-term [v s]
(->BlankNode
(rdf/->BlankNode
(l/-walk* s (:id v))))
l/IReifyTerm
(l/-reify-term [v s]
(l/-reify* s (:id v))))
(defn blank-node
"Returns a blank node. If no id is suplied a new (and unique) id will be generated."
([] (->BlankNode (gensym)))
([v]
(cond
(instance? BlankNode v) v
(blank-node? v) (->BlankNode (blank-node-id v))
:else (->BlankNode v))))
(declare ->Literal)
(defrecord Literal [value language datatype]
ILiteral
(literal-value [l] (:value l))
(literal-language [l] (:language l))
(literal-datatype [l] (:datatype l))
(extend-type rdf/Literal
l/IUnifyTerms
(-unify-terms [u v s]
(if (instance? Literal v)
(if (instance? rdf/Literal v)
(-> s
(l/unify (:value u) (:value v))
(l/unify (:language u) (:language v))
......@@ -174,7 +52,7 @@
l/IWalkTerm
(l/-walk-term [v s]
(->Literal
(rdf/->Literal
(l/-walk* s (:value v))
(l/-walk* s (:language v))
(l/-walk* s (:datatype v))))
......@@ -186,35 +64,10 @@
(l/-reify* (:language v))
(l/-reify* (:datatype v)))))
(defn literal
"Returns a literal with optional language and datatype."
([value & {:keys [language datatype]}]
(cond
;; already a literal, nothing to do
(instance? Literal value) value
;; satisfies the ILiteral protocol, cast to a Literal
(literal? value)
(->Literal (literal-value value)
(str (or language (literal-language value)))
(iri (or datatype (literal-datatype value))))
;; return a new Literal
:else (->Literal value language datatype))))
(declare ->Triple)
(defrecord Triple [subject predicate object]
ITriple
(triple-subject [q] (:subject q))
(triple-predicate [q] (:predicate q))
(triple-object [q] (:object q))
(extend-type rdf/Triple
l/IUnifyTerms
(l/-unify-terms [u v s]
(if (instance? Triple v)
(if (instance? rdf/Triple v)
(-> s
(l/unify (:subject u) (:subject v))
(l/unify (:predicate u) (:predicate v))
......@@ -223,7 +76,7 @@
l/IWalkTerm
(l/-walk-term [v s]
(->Triple
(rdf/->Triple
(l/-walk* s (:subject v))
(l/-walk* s (:predicate v))
(l/-walk* s (:object v))))
......@@ -235,63 +88,45 @@
(l/-reify* (:predicate v))
(l/-reify* (:object v)))))
(defn subject [s]
"Return an IRI or a BlankNode"
(cond
(satisfies? IIRI s) (iri s)
(satisfies? IBlankNode s) (blank-node s)
:else s))
(defn predicate [p]
"Returns an IRI"
(cond
(satisfies? IIRI p) (iri p)
:else p))
(defn object [o]
"Returns a Literal, IRI or BlankNode"
(cond
;; Cast as literal before casting as IRI. This causes (object-cast "hello") to return a literal instead of an IRI.
(satisfies? ILiteral o) (literal o)
(satisfies? IIRI o) (iri o)
(satisfies? IBlankNode o) (blank-node o)
:else o))
(defn triple
"Returns a triple."
([t] (cond
(instance? Triple t) t
(satisfies? ITriple t)
(->Triple
(subject (triple-subject t))
(predicate (triple-predicate t))
(object (triple-object t)))))
([s p o] (->Triple
(subject s)
(predicate p)
(object o))))
;; Logic helpers
(defn tripleo
"Relation to match triple"
[s p o t]
(fn [a]
(l/unify a (triple s p o) t)))
(l/unify a (rdf/triple s p o) t)))
(defn graph-tripleo
"Relation to match triples in a graph"
[graph q]
(fn [a]
(let [q (l/-walk* a q)]
(cond
(rdf/triple? q)
(l/to-stream
(map #(l/unify a % q)
(rdf/graph-match graph q)))
;; dump all triples
(l/lvar? q)
(l/to-stream (map #(l/unify a % q)
(rdf/graph-match graph
(rdf/triple (l/lvar) (l/lvar) (l/lvar)))))
;; will not match with anything else
:else (l/to-stream '())))))
(defn triple-subjecto
"Relation between triple and subject"
[t s]
(l/fresh [p o]
(tripleo s p o t)))
(tripleo s p o t)))
(defn triple-predicateo
"Relation between triple and predicate"
[t p]
(l/fresh [s o]
(tripleo s p o t)))
(tripleo s p o t)))
(defn triple-objecto
"Relation between triple and object"
......@@ -302,14 +137,14 @@
(defn typeo
"Relationship between subject and type"
[s rdf-type t]
(tripleo s (rdf :type) rdf-type t))
(tripleo s (ns/rdf :type) rdf-type t))
(defn graph-typeo
"Run typeo on a graph. This is useful for quickly getting the type of an object from a graph"
[graph s rdf-type]
(fresh [t]
(typeo s rdf-type t)
(graph-tripleo graph t)))
(typeo s rdf-type t)
(graph-tripleo graph t)))
(defn reachableo
"Is z reachable from a in graph withing n steps?"
......@@ -350,40 +185,3 @@
(collecto graph (dec n) o t))])
l/fail))
(defprotocol IDescription
"A Graph with a starting point"
(description-subject [this] "Returns the subject of the description.")
(description-get [this predicate] "Returns the objects for the given predicate.")
(description-move [this to] "Move the subject."))
(declare ->Description)
(defrecord Description [subject graph]
IGraph
(graph-add [this triple]
(->Description
(:subject this)
(graph-add (:graph this) triple)))
(graph-delete [this triple]
(->Description
(:subject this)
(graph-delete (:graph this) triple)))
(graph-has [this triple]
(graph-has (:graph this) triple))
(graph-match [this q]
(graph-match (:graph this) q))
(graph-tripleo [this q]
(graph-tripleo (:graph this) q))
IDescription
(description-subject [this]
(:subject this))
(description-get [this predicate]
(run* [o]
(graph-tripleo (:graph this) (triple (:subject this) predicate o))))
(description-move [this to]
(->Description to (:graph this))))
(defn description [subject graph]
(->Description subject graph))
(ns rdf.n3
"Bindings to the N3.js library for parsing Turtle"
(:require [rdf.core :as rdf]
["./n3.js" :as n3]
[cljs.core.async :as async :refer [<! >!]])
(:require-macros [cljs.core.async.macros :refer [go]]))
(def data-factory (.-DataFactory n3))
;; NamedNode
(def n3-named-node-type
(.-NamedNode (.-internal data-factory)))
(extend-type n3-named-node-type
rdf/IIRIConvert
(-as-iri [x] (rdf/iri (.-value x))))
;; TODO implement I*Convert for all RDF.js types and serialize with N3!
(defprotocol INamedNodeConvert
"Protocol for types that can be converted to NamedNode"
(-as-named-node [x]))
(extend-type rdf/IRI
INamedNodeConvert
(-as-named-node [x] (.namedNode data-factory (rdf/iri-value x))))
;; BlankNode
(def n3-blank-node-type
(.-BlankNode (.-internal (.-DataFactory n3))))
(extend-type n3-blank-node-type
rdf/IBlankNodeConvert
(-as-blank-node [x] (rdf/blank-node (.-value x))))
(defprotocol IBlankNodeConvert
(-as-blank-node [x]))
(extend-type rdf/BlankNode
IBlankNodeConvert
(-as-blank-node [x]