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 | (let loop ((superproperties (as-property-subproperty-of property))) |
| 295 | (match superproperties |
| 296 | (() #f) |
| 297 | ((superproperty superproperties ...) |
| 298 | (if (subproperty? superproperty other) |
| 299 | #t |
| 300 | (loop superproperties))))))) |
| 301 | |
| 302 | (define (subtype? type other) |
| 303 | "Is @code{type} a subtype of @code{other}?" |
| 304 | (or |
| 305 | (equal? type other) |
| 306 | (let loop ((supertypes (as-type-subclass-of type))) |
| 307 | (match supertypes |
| 308 | (() #f) |
| 309 | ((supertype supertypes ...) |
| 310 | (if (subtype? supertype other) |
| 311 | #t |
| 312 | (loop supertypes))))))) |
| 313 | |
| 314 | (define (as-ref document key) |
| 315 | "Takes a parsed document and returns the value associated with the property. |
| 316 | This takes care of subproperties: if you look for a property that's not in the |
| 317 | document directly, but the document has a subproperty of it, this will be |
| 318 | returned. The key must be a proper label as defined in the ontology." |
| 319 | (define (is-candidate kv) |
| 320 | (match kv |
| 321 | ((k . v) |
| 322 | (subproperty? k key)))) |
| 323 | (let ((candidates (filter is-candidate document))) |
| 324 | (map cdr candidates))) |
| 325 | |
| 326 | (define (json->as-document ontology document) |
| 327 | (define (uri->datatype type) |
| 328 | (let ((candidates (filter (lambda (t) (equal? (as-type-uri t) type)) |
| 329 | (ontology-datatypes ontology)))) |
| 330 | (cond |
| 331 | ((null? candidates) |
| 332 | #f) |
| 333 | ((> (length candidates) 1) |
| 334 | (throw 'multiple-datatypes-with-same-uri candidates)) |
| 335 | (else |
| 336 | (car candidates))))) |
| 337 | |
| 338 | (define (uri->property property) |
| 339 | (let ((candidates (filter (lambda (p) (equal? (as-property-uri p) property)) |
| 340 | (ontology-properties ontology)))) |
| 341 | (cond |
| 342 | ((null? candidates) |
| 343 | #f) |
| 344 | ((> (length candidates) 1) |
| 345 | (throw 'multiple-properties-with-same-uri candidates)) |
| 346 | (else (car candidates))))) |
| 347 | |
| 348 | (define (scalar->as-value value) |
| 349 | (cond |
| 350 | ((or (json-has-key? value "@language") (json-has-key? value "@direction")) |
| 351 | (make-as-string |
| 352 | (assoc-ref value "@value") |
| 353 | (assoc-ref value "@language") |
| 354 | (assoc-ref value "@direction"))) |
| 355 | ((json-has-key? value "@type") |
| 356 | (let* ((types (assoc-ref value "@type")) |
| 357 | (types (if (string? types) (list types) (array->list types))) |
| 358 | (types (map uri->datatype types))) |
| 359 | (make-as-typed-value (assoc-ref value "@value") types))) |
| 360 | (else |
| 361 | (assoc-ref value "@value")))) |
| 362 | |
| 363 | (cond |
| 364 | ((scalar? document) |
| 365 | document) |
| 366 | ((json-has-key? document "@value") |
| 367 | (scalar->as-value document)) |
| 368 | ((json-array? document) |
| 369 | ;; XXX: this filter is not correct if one of the values is the litteral |
| 370 | ;; "false" |
| 371 | (list->array 1 |
| 372 | (filter |
| 373 | (lambda (a) a) |
| 374 | (map (lambda (doc) (json->as-document ontology doc)) |
| 375 | (array->list document))))) |
| 376 | ((json-has-key? document "@type") |
| 377 | (let* ((types (assoc-ref document "@type")) |
| 378 | (types (if (string? types) (list types) (array->list types))) |
| 379 | (types (filter (lambda (a) a) (map uri->datatype types)))) |
| 380 | (if (null? types) |
| 381 | #f |
| 382 | (make-as-document |
| 383 | types |
| 384 | (filter |
| 385 | (lambda (a) a) |
| 386 | (map |
| 387 | (match-lambda |
| 388 | ((key . value) |
| 389 | (let ((property (uri->property key)) |
| 390 | (value (json->as-document ontology value))) |
| 391 | (if (and property (not (equal? key "@type")) value) |
| 392 | (cons (uri->property key) value) |
| 393 | #f)))) |
| 394 | document)))))) |
| 395 | (else |
| 396 | (map |
| 397 | (match-lambda |
| 398 | ((key . value) |
| 399 | (cons key (json->as-document ontology value)))) |
| 400 | document)))) |
| 401 | |
| 402 | (define (uri->as-document ontology uri) |
| 403 | (json->as-document ontology (expand uri))) |
| 404 | |
| 405 | (define (as-document->json ontology doc) |
| 406 | (define (as-document->proper-json doc) |
| 407 | (append |
| 408 | `(("@type" . ,(list->array 1 (map as-type-uri (as-document-types doc))))) |
| 409 | (map |
| 410 | (match-lambda |
| 411 | ((key . value) |
| 412 | (cons (as-property-uri key) (as-value->proper-json value)))) |
| 413 | (as-document-properties doc)))) |
| 414 | |
| 415 | (define (as-value->proper-json doc) |
| 416 | (cond |
| 417 | ((as-document? doc) |
| 418 | (as-document->proper-json doc)) |
| 419 | ((list? doc) |
| 420 | (map |
| 421 | (match-lambda |
| 422 | ((key . value) |
| 423 | (cons |
| 424 | (if (string? key) |
| 425 | key |
| 426 | (as-property-uri key)) |
| 427 | (as-value->proper-json value)))) |
| 428 | doc)) |
| 429 | ((string? doc) |
| 430 | doc) |
| 431 | ((array? doc) |
| 432 | (list->array 1 (map as-value->proper-json (array->list doc)))) |
| 433 | ((as-typed-value? doc) |
| 434 | `(("@value" . ,(as-typed-value-value doc)) |
| 435 | ("@type" . ,(as-type-uri (as-typed-value-type doc))))) |
| 436 | ((as-string? doc) |
| 437 | `(("@value" . ,(as-string-value doc)) |
| 438 | ,@(if (as-string-direction doc) |
| 439 | `(("@direction" . ,(as-string-direction doc))) |
| 440 | '()) |
| 441 | ,@(if (as-string-language doc) |
| 442 | `(("@language" . ,(as-string-language doc))) |
| 443 | '()))) |
| 444 | (else doc))) |
| 445 | |
| 446 | (compact (as-value->proper-json doc) |
| 447 | `(("@context" . ,(list->array 1 (ontology-context ontology)))))) |
| 448 | |
| 449 | (define* (as-document->graphviz doc #:key (label "n")) |
| 450 | (cond |
| 451 | ((as-document? doc) |
| 452 | (let* ((id (as-ref (as-document-properties doc) "@id")) |
| 453 | (id (if (string? id) id (if (null? id) "" (car id)))) |
| 454 | (types (as-document-types doc)) |
| 455 | (name (if (null? types) |
| 456 | id |
| 457 | (string-append id " (" |
| 458 | (string-join (map as-type-label types) ", ") |
| 459 | ")")))) |
| 460 | (format #t " ~a [label=\"~a\"];~%" label name) |
| 461 | (let loop ((children (as-document-properties doc)) (suffix 0)) |
| 462 | (match children |
| 463 | (() (format #t "~%")) |
| 464 | (((key . value) children ...) |
| 465 | (let ((child-label (string-append label (number->string suffix)))) |
| 466 | (format #t " ~a -> ~a [label=\"~a\"];~%" |
| 467 | label child-label |
| 468 | (if (string? key) key (as-property-label key))) |
| 469 | (as-document->graphviz value #:label child-label) |
| 470 | (loop children (+ suffix 1)))))))) |
| 471 | ((list? doc) |
| 472 | (format #t " ~a [label=\"\"];~%" label) |
| 473 | (let loop ((children doc) (suffix 0)) |
| 474 | (match children |
| 475 | (() (format #t "~%")) |
| 476 | (((key . value) children ...) |
| 477 | (let ((child-label (string-append label (number->string suffix)))) |
| 478 | (format #t " ~a -> ~a [label=\"~a\"];~%" |
| 479 | label child-label |
| 480 | (if (string? key) key (as-property-label key))) |
| 481 | (as-document->graphviz value #:label child-label) |
| 482 | (loop children (+ suffix 1))))))) |
| 483 | ((string? doc) |
| 484 | (format #t " ~a [label=\"~a\"];~%" label doc)) |
| 485 | ((array? doc) |
| 486 | (let loop ((children (array->list doc)) (suffix 0)) |
| 487 | (match children |
| 488 | (() (format #t "~%")) |
| 489 | ((value children ...) |
| 490 | (let ((child-label (string-append label (number->string suffix)))) |
| 491 | (format #t " ~a -> ~a;~%" label child-label) |
| 492 | (as-document->graphviz value #:label child-label) |
| 493 | (loop children (+ suffix 1))))))) |
| 494 | ((as-typed-value? doc) |
| 495 | (format #t " ~a [label=\"~a\"];~%" |
| 496 | label (string-append (as-typed-value-value doc) "^^" |
| 497 | (as-type-label (as-typed-value-type doc))))) |
| 498 | ((as-string? doc) |
| 499 | (let* ((str (as-string-value doc)) |
| 500 | (str (if (or (as-string-language doc) (as-string-direction doc)) |
| 501 | (string-append str "@") |
| 502 | str)) |
| 503 | (str (string-append str (as-string-language doc))) |
| 504 | (str (if (as-string-direction doc) |
| 505 | (string-append str "_" (as-string-direction doc)) |
| 506 | str))) |
| 507 | (format #t " ~a [label=\"~a\"];~%" |
| 508 | label str))) |
| 509 | (else doc))) |
| 510 |