Skip to content

Commit cc0395f

Browse files
committed
feat: improve handling of missing slot values
Signal an error when a mandatory slot receives no (valid) value upon creating an ODRL/SHACL object. Otherwise, missing or incorrect values would lead to unpredictable errors during conversion to ACL.
1 parent 598cbe5 commit cc0395f

3 files changed

Lines changed: 71 additions & 10 deletions

File tree

odrl/odrl.lisp

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212
;;
1313
;; Furthermore, this implementation explicitly deviates from ODRL's specification in some ways.
1414
;; Consult the documentation of individual classes for more information.
15+
16+
(defparameter supported-odrl-actions
17+
'("http://www.w3.org/ns/odrl/2/read"
18+
"http://www.w3.org/ns/odrl/2/modify"
19+
"http://www.w3.org/ns/odrl/2/write")
20+
"The absolute URIs of the ODRL actions we support in policies.")
21+
1522
(defclass concept ()
1623
((uri :initarg :uri
1724
:reader uri))
@@ -23,6 +30,14 @@
2330
:reader rules)) ; odrl:permission
2431
(:documentation "An ODRL Policy consisting of a set of rules."))
2532

33+
;; NOTE (08/04/2026): We use to `initialize-instance' to check arguments instead of `:initform' to
34+
;; allow more precise checks. For example, passing on `nil' as rules will result in unexpected
35+
;; behaviour.
36+
(defmethod initialize-instance :after ((policy policy) &key)
37+
(with-slots (rules) policy
38+
(unless (and rules (> (length rules) 0))
39+
(error "Must supply at least one RULE in a policy."))))
40+
2641
(defclass rule-set (policy)
2742
()
2843
(:documentation "An ODRL Set that represents any set of rules."))
@@ -56,7 +71,11 @@ simply be down cased."
5671
(:documentation "An ODRL party collection. In contrast to the ODRL specification this does not explicitly contain member parties. Instead members are essentially defined by the query, if the query returns a result the (implied) party is considered a member of the party collection."))
5772

5873
(defmethod initialize-instance :after ((concept party-collection) &key)
59-
(setf (slot-value concept 'name) (to-kebab-case (name concept))))
74+
(with-slots (name) concept
75+
(unless name
76+
(error "Must supply a NAME for a party collection."))
77+
78+
(setf (slot-value concept 'name) (to-kebab-case (name concept)))))
6079

6180
;; TODO: `define-graph' allows to specify extra options `:sparql' and `:delta'. The ODRL policy
6281
;; currently does not allow such options to be passed. Should extend data model to support this if
@@ -76,7 +95,15 @@ simply be down cased."
7695
(:documentation "An ODRL Asset collection representing a graph. In contrast to the ODRL specification this does explicitly refer to its contained assets, thereby modelling the inverse of the ODRL's partOf predicate. This inversion simplifies converting ODRL policies to ACL configurations as it allows to iterate of the necessary assets when given an asset collection, which is in turn referenced by a rule for the starting point of the ODRL to ACL conversion. Otherwise, one would somehow have to keep track of all asset instances and link them their collections. A consequence of this is that the entity creating `asset-collection' instances is responsible for inverting the relations between assets and the asset collections they part of. Furthermore, assets are represented as instances of `shacl:node-shape' and there is *no* explicit class for ODRL Assets."))
7796

7897
(defmethod initialize-instance :after ((concept asset-collection) &key)
79-
(setf (slot-value concept 'name) (to-kebab-case (name concept))))
98+
(with-slots (name graph assets) concept
99+
(unless name
100+
(error "Must supply a NAME for an asset collection."))
101+
(unless graph
102+
(error "Must supply a GRAPH (PREFIX) for an asset collection."))
103+
(unless (and assets (> (length assets) 0))
104+
(error "Must supply at least one ASSET that is part of an asset collection"))
105+
106+
(setf name (to-kebab-case (name concept)))))
80107

81108
(defclass rule (concept)
82109
((actions :initarg :actions
@@ -90,10 +117,24 @@ simply be down cased."
90117
:reader assignee)) ; odrl:assignee
91118
(:documentation "An ODRL rule combines the common parts for permissions, prohibitions, and duties. In contrast to the ODRL specification we allow a rule to specify multiple actions, as `acl::access-grant's allows multiple usages to be specified."))
92119

120+
(defmethod initialize-instance :after ((concept rule) &key)
121+
(with-slots (actions) concept
122+
(unless (and actions (> (length actions) 0))
123+
(error "Must supply at least one ACTION for a rule."))))
124+
93125
(defclass permission (rule)
94126
()
95127
(:documentation "An ODRL permission represents that an assignee is allowed to perform an action on a target."))
96128

129+
(defmethod initialize-instance :after ((concept permission) &key)
130+
(with-slots (actions target assignee) concept
131+
(unless (and actions (> (length actions) 0))
132+
(error "Must supply at least one ACTION for a permission."))
133+
(unless target
134+
(error "Must supply a TARGET asset collection for a permission."))
135+
(unless assignee
136+
(error "Must supply an ASSIGNEE party collection for a permission."))))
137+
97138
(defclass action (concept)
98139
()
99140
(:documentation "An ODRL Action class which indicates an operation that can be performed on an asset. The actual operation should be encoded in the URI of the action element. Note that the conversion to ACL currently only supports two actions: `odrl:read' and `odrl:modify', specifying any other action will lead to errors."))
@@ -180,7 +221,7 @@ simply be down cased."
180221
;; NOTE (23/01/2026): The odrl:write action was deprecated by odrl:modify. We will support it
181222
;; anyway for convenience.
182223
((cl-ppcre:scan ".*write>?$" uri) 'acl::write)
183-
(t (error "No matching right found for \"~a\"" uri)))))
224+
(t (error "Encountered a unsupported action \"~a\"" uri)))))
184225

185226
;;
186227
;; Varia

odrl/parse-ttl.lisp

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ If FILENAME is nil, fall back to \"config\" as default filename for the policy f
165165
"Find the uri for the policy resource defined in GRAPH."
166166
(car (list-resource-uris (type-uri :odrl-set) graph)))
167167

168+
(defun rdf-literal-value-maybe (literal)
169+
"Return the value of LITERLAL if it is an rdf literal object."
170+
(when (cl-ttl-parser:rdf-literal-p literal)
171+
(cl-ttl-parser:rdf-literal-value literal)))
172+
168173
;; NOTE (01/10/2025): These macros are used to make the init-forms in the `let' operators in the
169174
;; conversion functions more readable.
170175
(defmacro first-value-for-predicate (predicate graph)
@@ -241,13 +246,13 @@ Return nil if URI does not identify an rdf list element in GRAPH."
241246
(make-instance
242247
'party-collection
243248
:uri (uri-string uri)
244-
:name (cl-ttl-parser:rdf-literal-value name)
245-
:description (when description (cl-ttl-parser:rdf-literal-value description))
249+
:name (rdf-literal-value-maybe name)
250+
:description (rdf-literal-value-maybe description)
246251
:parameters (when parameters (parse-parameters parameters))
247252
;; TODO: Make sure to remove any newlines and/or trailing spaces at the end of the string;
248253
;; otherwise it will not be parsed correctly
249254
;; Also remove any newlines at the beginning of the string
250-
:query (when query (cl-ttl-parser:rdf-literal-value query))))))
255+
:query (rdf-literal-value-maybe query)))))
251256

252257
;; Asset Collections and Assets (Node shapes)
253258
(defun make-asset-collections (graph)
@@ -267,8 +272,8 @@ Return nil if URI does not identify an rdf list element in GRAPH."
267272
(make-instance
268273
'asset-collection
269274
:uri (uri-string uri)
270-
:name (cl-ttl-parser:rdf-literal-value name)
271-
:description (when description (cl-ttl-parser:rdf-literal-value description))
275+
:name (rdf-literal-value-maybe name)
276+
:description (rdf-literal-value-maybe description)
272277
:graph (uri-string graph-uri)
273278
:assets (mapcar
274279
(lambda (uri) (find-shape-with-uri uri assets))
@@ -340,7 +345,7 @@ ASSET-COL and PARTY-COL should be lists of, respectively, `asset-collection' and
340345
(make-instance
341346
'permission
342347
:uri (uri-string uri)
343-
:actions (list (make-action action))
348+
:actions (when action (list (make-action action)))
344349
:target target
345350
:assignee assignee)))
346351

odrl/shacl.lisp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,34 @@
2525
:reader notp))
2626
(:documentation "A SHACL node shape"))
2727

28+
(defmethod initialize-instance :after ((node node-shape) &key)
29+
(with-slots (target-class) node
30+
(unless target-class
31+
(error "Must supply a TARGET-CLASS for a node shape."))))
32+
2833
(defclass property-shape (shape)
2934
((path :initarg :path
30-
:initform nil
3135
:reader path)) ; value is a predicate URI or a `property-path' instance
3236
(:documentation "A SHACL property shape"))
3337

38+
(defmethod initialize-instance :after ((shape property-shape) &key)
39+
(with-slots (path) shape
40+
(unless path
41+
(error "Must supply a PATH for a property."))))
42+
3443
(defclass property-path ()
3544
((predicate-path :initarg :predicate-path
3645
:reader predicate-path)
3746
(object :initarg :object
3847
:reader object))
3948
(:documentation "A SHACL property path."))
4049

50+
(defmethod initialize-instance :after ((prop property-path) &key)
51+
(with-slots (predicate-path object) prop
52+
(unless predicate-path
53+
(error "Must supply a PREDICATE PATH for a property path."))
54+
(unless object
55+
(error "Must supply an OBJECT for a property path."))))
4156

4257
;;
4358
;; Conversion to sparql-parser's ACL

0 commit comments

Comments
 (0)