guile-fediverse/activitystreams/ontology.scm

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 #:key (options #f))
399
  (if options
400
      (json->as-document ontology (expand uri #:options options))
401
      (json->as-document ontology (expand uri))))
402
403
(define* (as-document->json ontology doc #:key (options #f))
404
  (define (as-document->proper-json doc)
405
    (append
406
      `(("@type" . ,(list->array 1 (map as-type-uri (as-document-types doc)))))
407
      (map
408
        (match-lambda
409
          ((key . value)
410
           (cons (as-property-uri key) (as-value->proper-json value))))
411
        (as-document-properties doc))))
412
413
  (define (as-value->proper-json doc)
414
    (cond
415
      ((as-document? doc)
416
       (as-document->proper-json doc))
417
      ((list? doc)
418
       (list->array 1 (map as-value->proper-json doc)))
419
      ((string? doc)
420
       doc)
421
      ((as-typed-value? doc)
422
       `(("@value" . ,(as-typed-value-value doc))
423
         ("@type" . ,(as-type-uri (as-typed-value-type doc)))))
424
      ((as-string? doc)
425
       `(("@value" . ,(as-string-value doc))
426
         ,@(if (as-string-direction doc)
427
               `(("@direction" . ,(as-string-direction doc)))
428
               '())
429
         ,@(if (as-string-language doc)
430
               `(("@language" . ,(as-string-language doc)))
431
               '())))
432
      (else doc)))
433
434
  (let ((proper-json (as-value->proper-json doc))
435
        (context `(("@context" . ,(list->array 1 (ontology-context ontology))))))
436
    (if options
437
        (compact proper-json context #:options options)
438
        (compact proper-json context))))
439
440
(define* (as-document->graphviz doc #:key (label "n"))
441
  (cond
442
    ((as-document? doc)
443
     (let* ((id (as-ref (as-document-properties doc) "@id"))
444
            (id (if (string? id) id (if (null? id) "" (car id))))
445
            (types (as-document-types doc))
446
            (name (if (null? types)
447
                      id
448
                      (string-append id " ("
449
                                     (string-join (map as-type-label types) ", ")
450
                                     ")"))))
451
       (format #t "  ~a [label=\"~a\"];~%" label name)
452
       (let loop ((children (as-document-properties doc)) (suffix 0))
453
         (match children
454
           (() (format #t "~%"))
455
           (((key . value) children ...)
456
            (let ((child-label (string-append label (number->string suffix))))
457
              (format #t "  ~a -> ~a [label=\"~a\"];~%"
458
                      label child-label
459
                      (if (string? key) key (as-property-label key)))
460
              (as-document->graphviz value #:label child-label)
461
              (loop children (+ suffix 1))))))))
462
    ((string? doc)
463
     (format #t "  ~a [label=\"~a\"];~%" label doc))
464
    ((list? doc)
465
     (let loop ((children doc) (suffix 0))
466
       (match children
467
         (() (format #t "~%"))
468
         ((value children ...)
469
          (let ((child-label (string-append label (number->string suffix))))
470
            (format #t "  ~a -> ~a;~%" label child-label)
471
            (as-document->graphviz value #:label child-label)
472
            (loop children (+ suffix 1)))))))
473
    ((as-typed-value? doc)
474
     (format #t "  ~a [label=\"~a\"];~%"
475
             label (string-append (as-typed-value-value doc) "^^"
476
                                (as-type-label (as-typed-value-type doc)))))
477
    ((as-string? doc)
478
     (let* ((str (as-string-value doc))
479
            (str (if (or (as-string-language doc) (as-string-direction doc))
480
                     (string-append str "@")
481
                     str))
482
            (str (string-append str (as-string-language doc)))
483
            (str (if (as-string-direction doc)
484
                     (string-append str "_" (as-string-direction doc))
485
                     str)))
486
     (format #t "  ~a [label=\"~a\"];~%"
487
             label str)))
488
    (else doc)))
489