ontology.scm
1 | ;;; Copyright (C) 2020 Julien Lepiller <julien@lepiller.eu> |
2 | ;;;; |
3 | ;;;; This library is free software; you can redistribute it and/or |
4 | ;;;; modify it under the terms of the GNU Lesser General Public |
5 | ;;;; License as published by the Free Software Foundation; either |
6 | ;;;; version 3 of the License, or (at your option) any later version. |
7 | ;;;; |
8 | ;;;; This library is distributed in the hope that it will be useful, |
9 | ;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | ;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
11 | ;;;; Lesser General Public License for more details. |
12 | ;;;; |
13 | ;;;; You should have received a copy of the GNU Lesser General Public |
14 | ;;;; License along with this library; if not, write to the Free Software |
15 | ;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
16 | ;;;; |
17 | |
18 | (define-module (activitystreams ontology) |
19 | #:use-module (ice-9 match) |
20 | #:use-module (srfi srfi-1) |
21 | #:use-module (srfi srfi-9) |
22 | #:use-module (jsonld) |
23 | #:use-module (jsonld json) |
24 | #:export (make-ontology |
25 | ontology? |
26 | ontology-context |
27 | ontology-datatypes |
28 | ontology-properties |
29 | |
30 | make-as-type |
31 | as-type? |
32 | as-type-label |
33 | as-type-uri |
34 | as-type-comment |
35 | as-type-subclass-of |
36 | build-as-type |
37 | |
38 | make-as-property |
39 | as-property? |
40 | as-property-label |
41 | as-property-uri |
42 | as-property-domain |
43 | as-property-range |
44 | as-property-functional? |
45 | as-property-subproperty-of |
46 | as-property-comment |
47 | build-as-property |
48 | |
49 | make-as-document |
50 | as-document? |
51 | as-document-types |
52 | as-document-properties |
53 | |
54 | make-as-string |
55 | as-string? |
56 | as-string-value |
57 | as-string-language |
58 | as-string-direction |
59 | |
60 | make-as-typed-value |
61 | as-typed-value? |
62 | as-typed-value-value |
63 | as-typed-value-type |
64 | |
65 | merge-ontologies |
66 | subproperty? |
67 | subtype? |
68 | as-ref |
69 | json->as-document |
70 | uri->as-document |
71 | as-document->json |
72 | as-document->graphviz)) |
73 | |
74 | (define-record-type <ontology> |
75 | (make-ontology context datatypes properties) |
76 | ontology? |
77 | (context ontology-context) |
78 | (datatypes ontology-datatypes) |
79 | (properties ontology-properties)) |
80 | |
81 | (define as-vocab "https://www.w3.org/ns/activitystreams#") |
82 | |
83 | (define-record-type as-type |
84 | (make-as-type label uri comment subclass-of) |
85 | as-type? |
86 | (label as-type-label) |
87 | (uri as-type-uri) |
88 | (comment as-type-comment) |
89 | (subclass-of as-type-subclass-of)) |
90 | |
91 | (define* (build-as-type label #:key (uri (string-append as-vocab label)) |
92 | (comment "") (subclass-of '())) |
93 | (make-as-type label uri comment subclass-of)) |
94 | |
95 | (define-record-type as-property |
96 | (make-as-property label uri range domain functional? subproperty-of comment) |
97 | as-property? |
98 | (label as-property-label) |
99 | (uri as-property-uri) |
100 | (domain as-property-domain) |
101 | (range as-property-range) |
102 | (functional? as-property-functional?) |
103 | (subproperty-of as-property-subproperty-of) |
104 | (comment as-property-comment)) |
105 | |
106 | (define* (build-as-property label domain range |
107 | #:key (uri (string-append as-vocab label)) |
108 | (functional? #f) (subproperty-of '()) (comment "")) |
109 | (make-as-property label uri range domain functional? subproperty-of comment)) |
110 | |
111 | (define-record-type as-document |
112 | (make-as-document types properties) |
113 | as-document? |
114 | (types as-document-types) |
115 | (properties as-document-properties)) |
116 | |
117 | (define-record-type as-string |
118 | (make-as-string value language direction) |
119 | as-string? |
120 | (value as-string-value) |
121 | (language as-string-language) |
122 | (direction as-string-direction)) |
123 | |
124 | (define-record-type as-typed-value |
125 | (make-as-typed-value value type) |
126 | as-typed-value? |
127 | (value as-typed-value-value) |
128 | (type as-typed-value-type)) |
129 | |
130 | (define (uniq lst) |
131 | (let loop ((lst lst) (result '())) |
132 | (match lst |
133 | (() result) |
134 | ((elem lst ...) |
135 | (if (member elem result) |
136 | (loop lst result) |
137 | (loop lst (cons elem result))))))) |
138 | |
139 | (define (alist-set lst key value) |
140 | (match lst |
141 | (() `((,key . ,value))) |
142 | (((k . v) lst ...) |
143 | (if (equal? k key) |
144 | (cons (cons key value) lst) |
145 | (cons (cons k v) (alist-set lst key value)))))) |
146 | |
147 | (define (merge-domains d1 d2) |
148 | (uniq |
149 | (if (list? d1) |
150 | (if (list? d2) |
151 | (append d1 d2) |
152 | (cons d2 d1)) |
153 | (if (list? d2) |
154 | (cons d1 d2) |
155 | (list d1 d2))))) |
156 | |
157 | (define (merge-ranges r1 r2) |
158 | (uniq |
159 | (if (list? r1) |
160 | (if (list? r2) |
161 | (append r1 r2) |
162 | (cons r2 r1)) |
163 | (if (list? r2) |
164 | (cons r1 r2) |
165 | (list r1 r2))))) |
166 | |
167 | (define (fix-types datatypes) |
168 | (define (fix-datatype type) |
169 | (if (as-type? type) |
170 | (let ((candidates |
171 | (filter (lambda (t) (equal? (as-type-uri t) (as-type-uri type))) |
172 | datatypes))) |
173 | (if (null? candidates) |
174 | type |
175 | (car candidates))) |
176 | type)) |
177 | |
178 | (let loop ((to-fix datatypes) (result '())) |
179 | (match to-fix |
180 | (() result) |
181 | ((type to-fix ...) |
182 | (loop |
183 | to-fix |
184 | (cons |
185 | (make-as-type |
186 | (as-type-label type) |
187 | (as-type-uri type) |
188 | (as-type-comment type) |
189 | (map fix-datatype (as-type-subclass-of type))) |
190 | result)))))) |
191 | |
192 | (define (merge-datatypes datatypes) |
193 | (let loop ((result '()) (datatypes (apply append datatypes))) |
194 | (match datatypes |
195 | (() (map cdr result)) |
196 | ((type datatypes ...) |
197 | (loop |
198 | (let ((previous (assoc-ref result (as-type-uri type)))) |
199 | (if previous |
200 | (alist-set result |
201 | (as-type-uri type) |
202 | (make-as-type |
203 | (as-type-label type) |
204 | (as-type-uri type) |
205 | (or (as-type-comment previous) (as-type-comment type)) |
206 | (uniq (append (as-type-subclass-of previous) |
207 | (as-type-subclass-of type))))) |
208 | (cons (cons (as-type-uri type) type) result))) |
209 | datatypes))))) |
210 | |
211 | (define (fix-properties datatypes properties) |
212 | (define (fix-datatype type) |
213 | (if (as-type? type) |
214 | (let ((candidates |
215 | (filter (lambda (t) (equal? (as-type-uri t) (as-type-uri type))) |
216 | datatypes))) |
217 | (if (null? candidates) |
218 | type |
219 | (car candidates))) |
220 | type)) |
221 | |
222 | (define (fix-property prop) |
223 | (if (as-property? prop) |
224 | (let ((candidates |
225 | (filter (lambda (p) (equal? (as-property-uri p) (as-property-uri prop))) |
226 | properties))) |
227 | (if (null? candidates) |
228 | prop |
229 | (car candidates))) |
230 | prop)) |
231 | |
232 | (let loop ((to-fix properties) (result '())) |
233 | (match to-fix |
234 | (() result) |
235 | ((prop to-fix ...) |
236 | (let ((domain (as-property-domain prop)) |
237 | (range (as-property-range prop))) |
238 | (loop |
239 | to-fix |
240 | (cons |
241 | (make-as-property |
242 | (as-property-label prop) |
243 | (as-property-uri prop) |
244 | (if (list? domain) |
245 | (map fix-property (map fix-datatype domain)) |
246 | (fix-property (fix-datatype domain))) |
247 | (if (list? range) |
248 | (map fix-property (map fix-datatype range)) |
249 | (fix-property (fix-datatype range))) |
250 | (as-property-functional? prop) |
251 | (map fix-property (as-property-subproperty-of prop)) |
252 | (as-property-comment prop)) |
253 | result))))))) |
254 | |
255 | (define (merge-properties properties) |
256 | (let loop ((result '()) (properties (apply append properties))) |
257 | (match properties |
258 | (() (map cdr result)) |
259 | ((prop properties ...) |
260 | (loop |
261 | (let ((previous (assoc-ref result (as-property-uri prop)))) |
262 | (if previous |
263 | (alist-set result |
264 | (as-property-uri prop) |
265 | (make-as-property |
266 | (as-property-label prop) |
267 | (as-property-uri prop) |
268 | (merge-domains (as-property-domain previous) |
269 | (as-property-domain prop)) |
270 | (merge-ranges (as-property-range previous) |
271 | (as-property-range prop)) |
272 | (and (as-property-functional? previous) |
273 | (as-property-functional? prop)) |
274 | (uniq (append (as-property-subproperty-of previous) |
275 | (as-property-subproperty-of prop))) |
276 | (or (as-property-comment previous) |
277 | (as-property-comment prop)))) |
278 | (cons (cons (as-property-uri prop) prop) result))) |
279 | properties))))) |
280 | |
281 | (define* (merge-ontologies . ontologies) |
282 | (let ((datatypes (merge-datatypes (map ontology-datatypes ontologies))) |
283 | (properties (merge-properties (map ontology-properties ontologies)))) |
284 | (make-ontology |
285 | (filter (lambda (a) a) (append-map ontology-context ontologies)) |
286 | (fix-types datatypes) |
287 | (fix-properties datatypes properties)))) |
288 | |
289 | (define (subproperty? property other) |
290 | "Is @code{property} a subproperty of @code{other}?" |
291 | (or |
292 | (equal? property other) |
293 | (equal? (as-property-uri property) other) |
294 | (and (as-property? other) |
295 | (equal? (as-property-uri property) (as-property-uri other))) |
296 | (let loop ((superproperties (as-property-subproperty-of property))) |
297 | (match superproperties |
298 | (() #f) |
299 | ((superproperty superproperties ...) |
300 | (if (subproperty? superproperty other) |
301 | #t |
302 | (loop superproperties))))))) |
303 | |
304 | (define (subtype? type other) |
305 | "Is @code{type} a subtype of @code{other}?" |
306 | (or |
307 | (equal? type other) |
308 | (let loop ((supertypes (as-type-subclass-of type))) |
309 | (match supertypes |
310 | (() #f) |
311 | ((supertype supertypes ...) |
312 | (if (subtype? supertype other) |
313 | #t |
314 | (loop supertypes))))))) |
315 | |
316 | (define (as-ref document key) |
317 | "Takes a parsed document and returns the value associated with the property. |
318 | This takes care of subproperties: if you look for a property that's not in the |
319 | document directly, but the document has a subproperty of it, this will be |
320 | returned. The key must be a proper label as defined in the ontology." |
321 | (define (is-candidate kv) |
322 | (match kv |
323 | ((k . v) |
324 | (subproperty? k key)))) |
325 | (let ((candidates (filter is-candidate |
326 | (if (as-document? document) |
327 | (as-document-properties document) |
328 | document)))) |
329 | (map cdr candidates))) |
330 | |
331 | (define (json->as-document ontology document) |
332 | (define (uri->datatype type) |
333 | (let ((candidates (filter (lambda (t) (equal? (as-type-uri t) type)) |
334 | (ontology-datatypes ontology)))) |
335 | (cond |
336 | ((null? candidates) |
337 | #f) |
338 | ((> (length candidates) 1) |
339 | (throw 'multiple-datatypes-with-same-uri candidates)) |
340 | (else |
341 | (car candidates))))) |
342 | |
343 | (define (uri->property property) |
344 | (let ((candidates (filter (lambda (p) (equal? (as-property-uri p) property)) |
345 | (ontology-properties ontology)))) |
346 | (cond |
347 | ((null? candidates) |
348 | #f) |
349 | ((> (length candidates) 1) |
350 | (throw 'multiple-properties-with-same-uri candidates)) |
351 | (else (car candidates))))) |
352 | |
353 | (define (scalar->as-value value) |
354 | (cond |
355 | ((or (json-has-key? value "@language") (json-has-key? value "@direction")) |
356 | (make-as-string |
357 | (assoc-ref value "@value") |
358 | (assoc-ref value "@language") |
359 | (assoc-ref value "@direction"))) |
360 | ((json-has-key? value "@type") |
361 | (let* ((types (assoc-ref value "@type")) |
362 | (types (if (string? types) (list types) (array->list types))) |
363 | (types (map uri->datatype types))) |
364 | (make-as-typed-value (assoc-ref value "@value") types))) |
365 | (else |
366 | (assoc-ref value "@value")))) |
367 | |
368 | (cond |
369 | ((scalar? document) |
370 | document) |
371 | ((json-has-key? document "@value") |
372 | (scalar->as-value document)) |
373 | ((json-array? document) |
374 | ;; XXX: this filter is not correct if one of the values is the litteral |
375 | ;; "false" |
376 | (filter |
377 | (lambda (a) a) |
378 | (map (lambda (doc) (json->as-document ontology doc)) |
379 | (array->list document)))) |
380 | ((list? document) |
381 | (let* ((types (or (assoc-ref document "@type") #())) |
382 | (types (if (string? types) (list types) (array->list types))) |
383 | (types (filter (lambda (a) a) (map uri->datatype types)))) |
384 | (make-as-document |
385 | types |
386 | (filter |
387 | (lambda (a) a) |
388 | (map |
389 | (match-lambda |
390 | ((key . value) |
391 | (let ((property (uri->property key)) |
392 | (value (json->as-document ontology value))) |
393 | (if (and property (not (equal? key "@type")) value) |
394 | (cons (uri->property key) value) |
395 | #f)))) |
396 | document))))))) |
397 | |
398 | (define (uri->as-document ontology uri) |
399 | (json->as-document ontology (expand uri))) |
400 | |
401 | (define (as-document->json ontology doc) |
402 | (define (as-document->proper-json doc) |
403 | (append |
404 | `(("@type" . ,(list->array 1 (map as-type-uri (as-document-types doc))))) |
405 | (map |
406 | (match-lambda |
407 | ((key . value) |
408 | (cons (as-property-uri key) (as-value->proper-json value)))) |
409 | (as-document-properties doc)))) |
410 | |
411 | (define (as-value->proper-json doc) |
412 | (cond |
413 | ((as-document? doc) |
414 | (as-document->proper-json doc)) |
415 | ((list? doc) |
416 | (map |
417 | (match-lambda |
418 | ((key . value) |
419 | (cons |
420 | (if (string? key) |
421 | key |
422 | (as-property-uri key)) |
423 | (as-value->proper-json value)))) |
424 | doc)) |
425 | ((string? doc) |
426 | doc) |
427 | ((array? doc) |
428 | (list->array 1 (map as-value->proper-json (array->list doc)))) |
429 | ((as-typed-value? doc) |
430 | `(("@value" . ,(as-typed-value-value doc)) |
431 | ("@type" . ,(as-type-uri (as-typed-value-type doc))))) |
432 | ((as-string? doc) |
433 | `(("@value" . ,(as-string-value doc)) |
434 | ,@(if (as-string-direction doc) |
435 | `(("@direction" . ,(as-string-direction doc))) |
436 | '()) |
437 | ,@(if (as-string-language doc) |
438 | `(("@language" . ,(as-string-language doc))) |
439 | '()))) |
440 | (else doc))) |
441 | |
442 | (compact (as-value->proper-json doc) |
443 | `(("@context" . ,(list->array 1 (ontology-context ontology)))))) |
444 | |
445 | (define* (as-document->graphviz doc #:key (label "n")) |
446 | (cond |
447 | ((as-document? doc) |
448 | (let* ((id (as-ref (as-document-properties doc) "@id")) |
449 | (id (if (string? id) id (if (null? id) "" (car id)))) |
450 | (types (as-document-types doc)) |
451 | (name (if (null? types) |
452 | id |
453 | (string-append id " (" |
454 | (string-join (map as-type-label types) ", ") |
455 | ")")))) |
456 | (format #t " ~a [label=\"~a\"];~%" label name) |
457 | (let loop ((children (as-document-properties doc)) (suffix 0)) |
458 | (match children |
459 | (() (format #t "~%")) |
460 | (((key . value) children ...) |
461 | (let ((child-label (string-append label (number->string suffix)))) |
462 | (format #t " ~a -> ~a [label=\"~a\"];~%" |
463 | label child-label |
464 | (if (string? key) key (as-property-label key))) |
465 | (as-document->graphviz value #:label child-label) |
466 | (loop children (+ suffix 1)))))))) |
467 | ((string? doc) |
468 | (format #t " ~a [label=\"~a\"];~%" label doc)) |
469 | ((list? doc) |
470 | (let loop ((children doc) (suffix 0)) |
471 | (match children |
472 | (() (format #t "~%")) |
473 | ((value children ...) |
474 | (let ((child-label (string-append label (number->string suffix)))) |
475 | (format #t " ~a -> ~a;~%" label child-label) |
476 | (as-document->graphviz value #:label child-label) |
477 | (loop children (+ suffix 1))))))) |
478 | ((as-typed-value? doc) |
479 | (format #t " ~a [label=\"~a\"];~%" |
480 | label (string-append (as-typed-value-value doc) "^^" |
481 | (as-type-label (as-typed-value-type doc))))) |
482 | ((as-string? doc) |
483 | (let* ((str (as-string-value doc)) |
484 | (str (if (or (as-string-language doc) (as-string-direction doc)) |
485 | (string-append str "@") |
486 | str)) |
487 | (str (string-append str (as-string-language doc))) |
488 | (str (if (as-string-direction doc) |
489 | (string-append str "_" (as-string-direction doc)) |
490 | str))) |
491 | (format #t " ~a [label=\"~a\"];~%" |
492 | label str))) |
493 | (else doc))) |
494 |