Skip to content

Commit

Permalink
Merge pull request #557 from fluree/feature/post-processing-validation
Browse files Browse the repository at this point in the history
post processing validation
  • Loading branch information
dpetran authored Aug 15, 2023
2 parents 73c3ab8 + 226a47e commit e4db5db
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 20 deletions.
83 changes: 69 additions & 14 deletions src/fluree/db/json_ld/shacl.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -183,15 +183,65 @@
(coalesce-validation-results results)))

(defn validate-value-properties
;; TODO: Only supports 'in' so far. Add the others.
[{:keys [in logical-constraint] :as _p-shape} p-flakes]
(let [results (for [flake p-flakes
:let [[val] (flake-value flake)
in-set (set in)]]
(if (in-set val)
[true (str "sh:not sh:in: value " val " must not be one of " in)]
[false (str "sh:in: value " val " must be one of " in)]))]
(coalesce-validation-results results logical-constraint)))
[{:keys [in has-value datatype nodekind logical-constraint] :as _p-shape} p-flakes]
(let [in-results (when in
(if (every? #(contains? (set in) (flake/o %)) p-flakes)
[true (str "sh:not sh:in: value " val " must not be one of " in)]
[false (str "sh:in: value " val " must be one of " in)]))
has-value-results (when has-value
(if (some #(= (flake/o %) has-value) p-flakes)
[true (str "sh:not sh:hasValue: none of the values can be " has-value)]
[false (str "sh:hasValue: at least one value must be " has-value)]))
datatype-results (when datatype
(if (every? #(= (flake/dt %) datatype) p-flakes)
[true (str "sh:not sh:datatype: every datatype must not be " datatype)]
[false (str "sh:datatype: every datatype must be " datatype)]))]
(coalesce-validation-results [in-results has-value-results datatype-results] logical-constraint)))


(defn validate-nodekind-constraint
[db {:keys [node-kind logical-constraint] :as _p-shape} p-flakes]
(go-try
(if (= node-kind const/$sh:Literal)
;; don't need to do a lookup to check for literals
(if (every? #(not= (flake/dt %) const/$xsd:anyURI) p-flakes)
[true "sh:not sh:nodekind: every value must not be a literal"]
[false "sh:nodekind: every value must be a literal"])

(loop [[f & r] p-flakes
res []]
(if f
(let [[id-flake] (<? (query-range/index-range db :spot = [(flake/o f) const/$xsd:anyURI]))
literal? (not= (flake/dt f) const/$xsd:anyURI)
bnode? (str/starts-with? (flake/o id-flake) "_:")
iri? (not (or literal? bnode?))
[valid? :as result]
(condp = node-kind
const/$sh:BlankNode
(if bnode?
[true "sh:not sh:nodekind: every value must not be a blank node identifier"]
[false "sh:nodekind: every value must be a blank node identifier"])
const/$sh:IRI
(if bnode?
[true "sh:not sh:nodekind: every value must not be an IRI"]
[false "sh:nodekind: every value must be an IRI"])
const/$sh:BlankNodeOrIRI
(if (or bnode? iri?)
[true "sh:not sh:nodekind: every value must not be a blank node identifier or an IRI"]
[false "sh:nodekind: every value must be a blank node identifier or an IRI"])
const/$sh:IRIOrLiteral
(if (or iri? literal?)
[true "sh:not sh:nodekind: every value must not be an IRI or a literal"]
[false "sh:nodekind: every value must be an IRI or a literal"])
const/$sh:BlankNodeOrLiteral
(if (or bnode? literal?)
[true "sh:not sh:nodekind: every value must not be a blank node identifier or a literal"]
[false "sh:nodekind: every value must be a blank node identifier or a literal"]))]
(if valid?
(recur r result)
;; short circuit if invalid
result))
res)))))

(declare build-node-shape)
(declare validate-shape)
Expand Down Expand Up @@ -235,8 +285,8 @@
(defn validate-property-constraints
"Validates a PropertyShape for a single predicate against a set of flakes.
Returns a tuple of [valid? error-msg]."
[{:keys [min-count max-count min-inclusive min-exclusive max-inclusive
max-exclusive min-length max-length pattern in node class] :as p-shape}
[{:keys [min-count max-count min-inclusive min-exclusive max-inclusive node-kind
max-exclusive min-length max-length pattern in has-value datatype node class] :as p-shape}
p-flakes
db]
(go-try
Expand All @@ -251,14 +301,18 @@
(or min-length max-length pattern))
(validate-string-properties p-shape p-flakes)
validation)
validation (if (and (first validation) in)
validation (if (and (first validation)
(or in has-value datatype))
(validate-value-properties p-shape p-flakes)
validation)
validation (if (and (first validation) node)
(<? (validate-node-constraint db p-shape p-flakes))
validation)
validation (if (and (first validation) class)
(<? (validate-class-properties db p-shape p-flakes))
validation)
validation (if (and (first validation) node-kind)
(<? (validate-nodekind-constraint db p-shape p-flakes))
validation)]
validation)))

Expand Down Expand Up @@ -590,10 +644,11 @@
{:dt datatype
:validate-fn validate-fn})

(defn register-nodetype
(defn register-nodekind
"Optimization to elevate node type designations"
[{:keys [dt validate-fn] :as dt-map} {:keys [class node-kind path] :as property-shape}]
(let [dt-map* (condp = node-kind

const/$sh:BlankNode
{:dt const/$xsd:anyURI
:class class
Expand Down Expand Up @@ -754,7 +809,7 @@

(:node-kind property-shape)
(update-in [:datatype target-key]
register-nodetype property-shape))]
register-nodekind property-shape))]
(recur r' shape* p-shapes*))
(let [shape* (condp = p
const/$xsd:anyURI
Expand Down
85 changes: 79 additions & 6 deletions test/fluree/db/shacl/shacl_basic_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -842,7 +842,7 @@
:ex/birthYear 1984}]
@(fluree/query db-ok-birthyear user-query))))))

(deftest shacl-multiple-properties-test
(deftest ^:integration shacl-multiple-properties-test
(testing "multiple properties works"
(let [conn (test-utils/create-conn)
ledger @(fluree/create conn "shacl/b" {:defaultContext ["" {:ex "http://example.org/ns/"}]})
Expand Down Expand Up @@ -926,7 +926,7 @@
:schema/name "John"}]
@(fluree/query db-ok user-query))))))

(deftest property-paths
(deftest ^:integration property-paths
(let [conn @(fluree/connect {:method :memory})
ledger @(fluree/create conn "propertypathstest" {:defaultContext [test-utils/default-str-context {"ex" "http://example.com/"}]})
db0 (fluree/db ledger)]
Expand Down Expand Up @@ -1011,7 +1011,7 @@
(is (= "SHACL PropertyShape exception - sh:minCount of 1 higher than actual count of 0."
(ex-message invalid-princess)))))))

(deftest shacl-class-test
(deftest ^:integration shacl-class-test
(let [conn @(fluree/connect {:method :memory})
ledger @(fluree/create conn "classtest" {:defaultContext test-utils/default-str-context})
db0 (fluree/db ledger)
Expand Down Expand Up @@ -1077,7 +1077,7 @@
(is (util/exception? db5))
(is (str/starts-with? (ex-message db5) "SHACL PropertyShape exception - sh:class"))))

(deftest shacl-in-test
(deftest ^:integration shacl-in-test
(testing "value nodes"
(let [conn @(fluree/connect {:method :memory
:defaults
Expand Down Expand Up @@ -1147,7 +1147,7 @@
(is (util/exception? db2))
(is (str/includes? (ex-message db2) "sh:in")))))

(deftest shacl-targetobjectsof-test
(deftest ^:integration shacl-targetobjectsof-test
(testing "subject and object of constrained predicate in the same txn"
(testing "datatype constraint"
(let [conn @(fluree/connect {:method :memory
Expand Down Expand Up @@ -1312,7 +1312,7 @@
(is (util/exception? db-forbidden-friend))
(is (str/includes? (ex-message db-forbidden-friend) "data type"))))))

(deftest shape-based-constraints
(deftest ^:integration shape-based-constraints
(testing "sh:node"
(let [conn @(fluree/connect {:method :memory})
ledger @(fluree/create conn "shape-constaints" {:defaultContext [test-utils/default-str-context
Expand Down Expand Up @@ -1477,3 +1477,76 @@
(is (util/exception? invalid-hand))
(is (= "SHACL PropertyShape exception - path [[1003 :predicate]] conformed to sh:qualifiedValueShape fewer than sh:qualifiedMinCount times."
(ex-message invalid-hand))))))

(deftest ^:integration post-processing-validation
(let [conn @(fluree/connect {:method :memory})
ledger @(fluree/create conn "post-processing" {:defaultContext [test-utils/default-str-context
{"ex" "http://example.com/"}]})
db0 (fluree/db ledger)]
(testing "shacl-objects-of-test"
(let [db1 @(fluree/stage db0
[{"@id" "ex:friendShape"
"type" ["sh:NodeShape"]
"sh:targetObjectsOf" {"@id" "ex:friend"}
"sh:property" [{"sh:path" {"@id" "ex:name"}
"sh:datatype" {"@id" "xsd:string"}}]}])
db2 @(fluree/stage db1 [{"id" "ex:Bob"
"ex:name" 123
"type" "ex:User"}])
db-forbidden-friend @(fluree/stage db2
{"id" "ex:Alice"
"type" "ex:User"
"ex:friend" {"@id" "ex:Bob"}})]
(is (util/exception? db-forbidden-friend))
(is (= "SHACL PropertyShape exception - sh:datatype: every datatype must be 1."
(ex-message db-forbidden-friend)))))

(testing "shape constraints"
(let [db1 @(fluree/stage db0 [{"id" "ex:CoolShape"
"type" "sh:NodeShape"
"sh:property" [{"sh:path" {"id" "ex:isCool"}
"sh:hasValue" true
"sh:minCount" 1}]}
{"id" "ex:PersonShape"
"type" "sh:NodeShape"
"sh:targetClass" {"id" "ex:Person"}
"sh:property" [{"sh:path" {"id" "ex:cool"}
"sh:node" {"id" "ex:CoolShape"}
"sh:minCount" 1}]}])
valid-person @(fluree/stage db1 [{"id" "ex:Bob"
"type" "ex:Person"
"ex:cool" {"ex:isCool" true}}])
invalid-person @(fluree/stage db1 [{"id" "ex:Reto"
"type" "ex:Person"
"ex:cool" {"ex:isCool" false}}])]
(is (= [{"id" "ex:Bob",
"type" "ex:Person",
"ex:cool" {"id" "_:f211106232532997", "ex:isCool" true}}]
@(fluree/query valid-person {"select" {"?s" ["*" {"ex:cool" ["*"]}]}
"where" [["?s" "id" "ex:Bob"]]})))
(is (util/exception? invalid-person))
(is (= "SHACL PropertyShape exception - sh:hasValue: at least one value must be true."
(ex-message invalid-person)))))
(testing "extended path constraints"
(let [db1 @(fluree/stage db0 [{"id" "ex:PersonShape"
"type" "sh:NodeShape"
"sh:targetClass" {"id" "ex:Person"}
"sh:property" [{"sh:path" [{"id" "ex:cool"} {"id" "ex:dude"}]
"sh:nodeKind" {"id" "sh:BlankNode"}
"sh:minCount" 1}]}])
valid-person @(fluree/stage db1 [{"id" "ex:Bob"
"type" "ex:Person"
"ex:cool" {"ex:dude" {"ex:isBlank" true}}}])
invalid-person @(fluree/stage db1 [{"id" "ex:Reto"
"type" "ex:Person"
"ex:cool" {"ex:dude" {"id" "ex:Dude"
"ex:isBlank" false}}}])]
(is (= [{"id" "ex:Bob",
"type" "ex:Person",
"ex:cool" {"id" "_:f211106232532995",
"ex:dude" {"id" "_:f211106232532996", "ex:isBlank" true}}}]
@(fluree/query valid-person {"select" {"?s" ["*" {"ex:cool" ["*" {"ex:dude" ["*"]}]}]}
"where" [["?s" "id" "ex:Bob"]]})))
(is (util/exception? invalid-person))
(is (= "SHACL PropertyShape exception - sh:nodekind: every value must be a blank node identifier."
(ex-message invalid-person)))))))

0 comments on commit e4db5db

Please sign in to comment.