ojak_vocab/read.rs
1//! Reading documents written by other servers, and writing Ojak's own.
2
3use alloc::{string::String, vec::Vec};
4use core::fmt;
5use serde::de::DeserializeOwned;
6
7use crate::json::{FromJson, JsonError, ToJson};
8use crate::loss::{self, Loss};
9use serde_json::Value;
10
11pub use ojak_jsonld::Registry;
12
13/// A document read into a vocabulary type.
14#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct Read<T> {
16 value: T,
17 unresolved_contexts: Vec<String>,
18 lost: Vec<Loss>,
19}
20
21impl<T> Read<T> {
22 /// The value that was read.
23 #[must_use]
24 pub fn value(&self) -> &T {
25 &self.value
26 }
27
28 /// The value that was read, by value.
29 pub fn into_value(self) -> T {
30 self.value
31 }
32
33 /// Context IRIs the document named that Ojak does not ship.
34 ///
35 /// Not an error, but what they define was not resolved: a key relying on
36 /// them keeps the sender's spelling, so no field of Ojak's types reads
37 /// it. Worth logging when a peer's documents read emptier than expected.
38 #[must_use]
39 pub fn unresolved_contexts(&self) -> &[String] {
40 &self.unresolved_contexts
41 }
42
43 /// What the document said that the value does not: properties the
44 /// vocabulary does not define, and values that were not the shape their
45 /// property allows. Only [`read_reporting`] fills it.
46 ///
47 /// Not an error: reading is tolerant so that one odd property does not
48 /// cost the whole object. Worth logging, and worth a look when a peer's
49 /// documents read emptier than expected.
50 #[must_use]
51 pub fn lost(&self) -> &[Loss] {
52 &self.lost
53 }
54}
55
56/// Why a document could not be read.
57#[derive(Debug)]
58pub enum ReadError {
59 /// The document is not JSON-LD Ojak will process.
60 JsonLd(ojak_jsonld::Error),
61 /// The document was processed but is not the shape of the type asked for.
62 Shape(serde_json::Error),
63 /// The document was processed but is not a value of the type asked for.
64 Value(JsonError),
65}
66
67impl fmt::Display for ReadError {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 match self {
70 Self::JsonLd(error) => write!(f, "cannot process the document: {error}"),
71 Self::Shape(error) => write!(f, "the document is not the expected shape: {error}"),
72 Self::Value(error) => write!(f, "the document is not the expected type: {error}"),
73 }
74 }
75}
76
77impl core::error::Error for ReadError {}
78
79/// Read a document from another server into a vocabulary type.
80///
81/// The document is normalised first, so it is read by what its keys mean
82/// rather than how the sender spelled them: `"as:sensitive"`, a term aliased
83/// to it, and `"sensitive"` all arrive in the same field, and a type written
84/// with a prefix, such as `"fep:QuoteRequest"`, arrives as the type it names.
85/// Only the contexts `registry` holds are resolved; nothing is fetched.
86///
87/// Verify signatures and proofs on `document` before reading it, not on the
88/// value this returns. Normalising rewrites the document, and a signature
89/// covers the bytes that were signed.
90///
91/// # Errors
92///
93/// [`ReadError::JsonLd`] when the document cannot be processed — it uses
94/// `@graph`, `@included` or `@reverse`, or exceeds a processing limit — and
95/// [`ReadError::Shape`] when it is not the shape of `T`.
96pub fn read<T: DeserializeOwned>(
97 registry: &Registry,
98 document: &Value,
99) -> Result<Read<T>, ReadError> {
100 let processed = ojak_jsonld::normalize(registry, document).map_err(ReadError::JsonLd)?;
101 let unresolved_contexts = processed.unresolved_contexts().to_vec();
102 let mut normalized = processed.into_document();
103 // The context is now Ojak's own, whatever the sender wrote. The types
104 // carry their own and must not be handed this one.
105 if let Value::Object(members) = &mut normalized {
106 members.remove("@context");
107 }
108 let value = serde_json::from_value(normalized).map_err(ReadError::Shape)?;
109 Ok(Read {
110 value,
111 unresolved_contexts,
112 lost: Vec::new(),
113 })
114}
115
116/// [`read`], and say what the value did not keep: see [`Read::lost`].
117///
118/// # Errors
119///
120/// As [`read`].
121pub fn read_reporting<T: FromJson + ToJson>(
122 registry: &Registry,
123 document: &Value,
124) -> Result<Read<T>, ReadError> {
125 let processed = ojak_jsonld::normalize(registry, document).map_err(ReadError::JsonLd)?;
126 let unresolved_contexts = processed.unresolved_contexts().to_vec();
127 let mut normalized = processed.into_document();
128 if let Value::Object(members) = &mut normalized {
129 members.remove("@context");
130 }
131 let value = T::from_json(&normalized).map_err(ReadError::Value)?;
132 let lost = loss::losses(&normalized, &value.to_json());
133 Ok(Read {
134 value,
135 unresolved_contexts,
136 lost,
137 })
138}
139
140/// Write a vocabulary value as a document: in Ojak's spelling, under Ojak's
141/// context.
142///
143/// This is the counterpart of [`read`], and only for the top of a document.
144/// A value's own [`ToJson`] writes no `@context`, because an object embedded
145/// in another takes its context from the document around it.
146#[must_use]
147pub fn write<T: ToJson>(value: &T) -> Value {
148 let mut document = value.to_json();
149 if let Value::Object(members) = &mut document {
150 members.insert("@context".into(), ojak_jsonld::ojak_context());
151 }
152 document
153}
154
155/// Write a vocabulary value as a document under `context` rather than
156/// Ojak's: the plain ActivityStreams context, say, which is what Mastodon
157/// writes its activities under. `@context` comes first, wherever a map that
158/// keeps its keys in order writes them.
159#[must_use]
160pub fn write_with_context<T: ToJson>(value: &T, context: Value) -> Value {
161 let mut document = serde_json::Map::new();
162 document.insert("@context".into(), context);
163 if let Value::Object(members) = value.to_json() {
164 document.extend(members);
165 }
166 Value::Object(document)
167}