Skip to main content

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}