Skip to content

Instantly share code, notes, and snippets.

@EmmanuelOga
Last active April 25, 2020 10:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save EmmanuelOga/2fcb3a76ad58c0a73122544306d4b3fb to your computer and use it in GitHub Desktop.
Save EmmanuelOga/2fcb3a76ad58c0a73122544306d4b3fb to your computer and use it in GitHub Desktop.
Validate ShEx from Clojure usign the Shaclex scala library and Jena. Started as a port of https://github.com/weso/simpleShExScala/blob/master/src/main/scala/es/weso/simpleShEx/Main.scala
{:paths
["src" "test" "target" "resources"]
:mvn/repos
{"bintray" {:url "https://dl.bintray.com/labra/maven/"}}
:deps
{; Clojure
org.clojure/clojure {:mvn/version "1.10.1"}
; Shaclex
es.weso/shaclex_2.13 {:mvn/version "0.1.44" :extension "pom"}
; Jena
org.apache.jena/jena {:mvn/version "3.13.1" :extension "pom"}}}
[{:node "<http://example.org/alice>",
:shape "<http://example.org/User>",
:status "conformant",
:appInfo "Shaclex",
:reason
"Alice has datatype xsd:string\nschema:Female is equal to schema:Female\nschema:Female passes OR\n:bob is an IRI"}
{:node "<http://example.org/bob>",
:shape "<http://example.org/User>",
:status "conformant",
:appInfo "Shaclex",
:reason
"Robert has datatype xsd:string\n1980-03-10 has datatype xsd:date\nschema:Male is equal to schema:Male\nschema:Male passes OR\nRobert has datatype xsd:string\n1980-03-10 has datatype xsd:date\nschema:Male is equal to schema:Male\nschema:Male passes OR"}
{:node "<http://example.org/carol>",
:shape "<http://example.org/User>",
:status "conformant",
:appInfo "Shaclex",
:reason
"Carol has datatype xsd:string\nunspecified has datatype xsd:string\nunspecified passes OR"}]
@prefix : <http://example.org/>
@prefix schema: <http://schema.org/>
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>
@prefix foaf: <http://xmlns.com/foaf/0.1/>
:alice
schema:name "Alice" ;
schema:gender schema:Female ;
schema:knows :bob .
:bob schema:gender schema:Male ;
schema:name "Robert";
schema:birthDate "1980-03-10"^^xsd:date .
:carol schema:name "Carol";
schema:gender "unspecified" ;
foaf:name "Carol" .
PREFIX : <http://example.org/>
PREFIX schema: <http://schema.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
:User {
schema:name xsd:string ;
schema:birthDate xsd:date? ;
schema:gender [ schema:Male schema:Female ] OR xsd:string ;
schema:knows IRI @:User*
}
(ns rdfex.jena
(:require [clojure.java.io :as io])
(:import [org.apache.jena.rdf.model Model ModelFactory]))
(def ^:dynamic rdf-base-url
"https://emmanueloga.com")
(defn create-empty-model
[]
(ModelFactory/createDefaultModel))
(defn parse
([input] (parse input rdf-base-url "TURTLE"))
([input base-url format]
(let [model (create-empty-model)
input-stream (io/input-stream input)]
(.read model input-stream base-url format))))
(defn write
([model] (write model "JSON-LD"))
([model format]
(let [buffer (java.io.ByteArrayOutputStream.)]
(.write model buffer format)
(str buffer))))
(def ^:dynamic *max-print-prefixes* 50)
; Extend print to understand Jena models.
(defmethod print-method Model [v ^java.io.Writer w]
(let [graph (.getGraph v)
sample (create-empty-model)
size (.size graph)
statements (iterator-seq (.listStatements v))]
(.setNsPrefixes sample (.getPrefixMapping graph) )
(.write w (str "Jena, " size " statements. Sample:\n\n"))
(run! (fn [s] (.add sample s)) (take *max-print-prefixes* statements))
(.write w (write sample "N3"))))
(ns rdfex.shex
(:require [clojure.data.json :as json]
[clojure.java.io :as io]
[rainbowfish.jena :as jena])
(:import es.weso.rdf.jena.RDFAsJenaModel
es.weso.shapeMaps.ShapeMap$
es.weso.shex.Schema$
es.weso.shex.validator.Validator$
org.apache.jena.rdf.model.Model
[scala Option Some]))
(defn $either
"Attempt to return the right side of a scala Either type.
May raise a left if the right is not available."
[either]
(try
(-> either (.right) (.get))
(catch Exception e
(throw (ex-info "Unexpected value" {:value (-> either (.left) (.get))})))))
(defn $wrap
"Wrap a value in a scala Option: either empty or (Some. value)"
[value]
(if value (Some. value) (Option/empty)))
(defn get-srdf
"Shaclex uses Srdf, which wraps Jena in a custom interface"
([model]
(get-srdf model nil nil))
([model base src-iri]
(cond
(instance? org.apache.jena.rdf.model.Model model)
(RDFAsJenaModel. model ($wrap base) ($wrap src-iri))
:else
(throw (ex-info "Unsupported model" {::model model})))))
(defn parse-shex
"Parses the Shex schema provided on the src on a given format"
([^String src]
(parse-shex src "ShexC" nil nil))
([^String src format schema-name base]
($either
(.fromString Schema$/MODULE$ src "ShexC" ($wrap schema-name) ($wrap base)))))
(defn parse-shape-map
"Parses the shape map on the src with the given format"
([^String src srdf-pfx shex-pfx]
(parse-shape-map
src (.defaultFormat ShapeMap$/MODULE$) srdf-pfx shex-pfx nil))
([^String src format srdf-pfx shex-pfx base]
($either
(.fromString ShapeMap$/MODULE$ src format ($wrap base) srdf-pfx shex-pfx))))
(defn fix-shape-map
"Fixes the shape map into a form usable by the validator"
[shape-map srdf srdf-pfx shex-pfx]
($either
(.fixShapeMap ShapeMap$/MODULE$ shape-map srdf srdf-pfx shex-pfx)))
(defn validate
"Validates an RDF model with a given shex schema and shape map"
[model ^String shex-compact ^String shape-map]
(let [srdf (get-srdf model)
shex (parse-shex shex-compact)
srdf-pfx (.getPrefixMap srdf)
shex-pfx (.prefixMap shex)
shape-map (parse-shape-map shape-map srdf-pfx shex-pfx)
fixed-shape-map (fix-shape-map shape-map srdf srdf-pfx shex-pfx)]
(->
($either (.validate Validator$/MODULE$ shex fixed-shape-map srdf))
(.toJson)
(.toString)
(json/read-str :key-fn keyword))))
; Example:
(let result
(let [model (jena/parse (io/resource "sandbox/example.ttl"))
result (validate-jena
model
(slurp (io/resource "schemas/example.shex"))
":alice@:User,:bob@:User,:carol@:User")]
result))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment