activity-pub-poc/src/main/clj/lambdaisland/souk/json_ld.clj

174 lines
5.6 KiB
Clojure
Raw Normal View History

2023-06-17 06:34:35 +00:00
(ns lambdaisland.souk.json-ld
"Interfacing with JSON-LD endpoints, and converting to and from idiomatic
Clojure data."
(:require [hato.client :as hato]
[clojure.string :as str]
[clojure.walk :as walk]))
2023-06-16 15:38:51 +00:00
2023-06-15 13:00:46 +00:00
(defn json-get [url]
2023-06-17 06:34:35 +00:00
(hato/get url
{:headers {"Accept" "application/json"}
:http-client {:redirect-policy :normal}
:as :json-string-keys}))
(def context-cache (atom {})) ;; url -> context
2023-06-16 14:06:28 +00:00
2023-06-16 15:38:51 +00:00
(defn fetch-context [url]
(if-let [ctx (get @context-cache url)]
ctx
(get
(swap! context-cache
(fn [cache]
2023-06-17 06:34:35 +00:00
(assoc cache url (get (:body (json-get url)) "@context"))))
2023-06-16 15:38:51 +00:00
url)))
(defn expand-context
([new-context]
(expand-context {} new-context))
([current-context new-context]
(cond
(string? new-context)
(expand-context current-context (fetch-context new-context))
(sequential? new-context)
(reduce expand-context current-context new-context)
(map? new-context)
(into current-context
(map
(fn [[k v]]
(let [id (if (map? v) (get v "@id" v) v)
[prefix suffix] (str/split id #":")]
(if-let [base (get (merge current-context new-context) prefix)]
[k (assoc (if (map? v) v {})
"@id" (str (if (map? base) (get base "@id") base)
suffix))]
[k (if (map? v) v {"@id" v})]))))
new-context))))
2023-06-16 16:42:59 +00:00
(defn expand-id [id ctx]
(if (string? id)
(if-let [t (get-in ctx [id "@id"])]
t
(if (str/includes? id ":")
(let [[prefix suffix] (str/split id #":")]
(if-let [prefix-url (get-in ctx [prefix "@id"])]
(str prefix-url suffix)
id))
id))
id))
(defn apply-context [v ctx]
(cond
(map? v)
(into {}
(map (fn [[k v]]
(let [attr (get ctx k)
k (get-in ctx [k "@id"] k)
v (apply-context v ctx)]
[k (if attr
(cond
(and (#{"@id" "@type"} k) (string? v))
(expand-id v ctx)
(and (= "@id" (get attr "@type")) (string? v))
(assoc attr "@id" (expand-id v ctx))
:else
(assoc (dissoc (cond-> attr
(contains? attr "@type")
(update "@type" expand-id ctx))
"@id")
"@value" v))
v)])))
v)
2023-06-17 06:34:35 +00:00
2023-06-16 16:42:59 +00:00
(sequential? v)
(into (empty v) (map #(apply-context % ctx)) v)
(and (string? v) (str/includes? v ":"))
(expand-id v ctx)
:else
v))
(defn expand [json-ld]
(let [ctx (expand-context {} (get json-ld "@context"))]
(apply-context (dissoc json-ld "@context") ctx)))
(defn internalize [v prefixes]
(let [shorten #(if (and (string? %) (str/includes? % "://"))
(or (some (fn [[ns url]]
(when (.startsWith % url)
(keyword ns
(subs % (.length url)))))
prefixes)
%)
%)]
(cond
(sequential? v)
(into (empty v) (map #(internalize % prefixes)) v)
(map? v)
(-> v
(cond-> (contains? v "@type")
(update "@type" shorten))
(update-keys (fn [k]
(case k
"@id" :rdf/id
"@type" :rdf/type
"@value" :rdf/value
(if-let [kw (shorten k)]
kw
k))))
(update-vals (fn [v]
(cond
(map? v)
(let [{id "@id" type "@type" value "@value"} v]
(cond
(and (= "@id" type) (contains? v "@id"))
id
(contains? v "@value")
(case type
"http://www.w3.org/2001/XMLSchema#dateTime"
(java.time.ZonedDateTime/parse value)
(internalize value prefixes))
:else
(internalize v prefixes)))
(string? v)
(shorten v)
:else
(internalize v prefixes)))))
:else
v)))
(defn kw->iri [kw prefixes]
(if (keyword? kw)
(let [prefix (namespace kw)
base (get prefixes prefix)]
(if base
(str base (name kw))
kw))
kw))
(defn externalize [v context prefixes]
(let [iri->prop (into {}
(map (fn [[k v]]
[(get v "@id") k]))
(expand-context context))
convert-val (fn [v]
(let [v (cond
(= :rdf/type v) "@type"
(= :rdf/id v) "@id"
:else v)
?iri (kw->iri v prefixes)]
(get iri->prop ?iri ?iri)))]
(assoc (walk/postwalk convert-val v) "@context" context)))
2023-06-16 15:38:51 +00:00
2023-06-17 06:34:35 +00:00
;; (compact
;; (expand (:body (json-get "https://toot.cat/users/plexus")))
;; common-prefixes)
2023-06-16 16:42:59 +00:00
2023-06-17 06:34:35 +00:00
;; "name"
;; "profileName"