Skip to main content

jsonld_expandable_core/
lib.rs

1//! Runtime support for the JSON-LD `Expandable` derive macro.
2//!
3//! This crate hosts:
4//!
5//! - The [`Expandable`] trait that the derive macro implements.
6//! - The [`JsonValue`] backend trait abstracting any JSON value type the
7//!   derive can emit (`serde_json::Value`, `sonic_rs::Value`,
8//!   `jstrict::Value`, or a user-supplied type).
9//! - The [`ToJsonValue`] trait used by generated code to convert leaf field
10//!   values into a chosen [`JsonValue`] backend.
11//! - Behind the `codegen` feature: the attribute parser, intermediate
12//!   representation, and codegen used by the `jsonld-expandable` proc-macro
13//!   crate (kept here so they can be unit-tested without `trybuild`). Only
14//!   the derive crate enables the feature; runtime consumers never pull in
15//!   syn/quote/proc-macro2, and those modules are internal API for the
16//!   derive, not covered by semver.
17//!
18//! Application code rarely depends on this crate directly; instead enable an
19//! `expandable*` feature on the umbrella `jsonld` crate, which re-exports both
20//! the derive and this crate (as `jsonld::expandable_core`). The derive reads
21//! the consuming crate's manifest and points the code it generates at whichever
22//! of the two that crate depends on, following a renamed dependency to the name
23//! it was given.
24//!
25//! # Limitations
26//!
27//! The derive does not add trait bounds for generic parameters: a generic
28//! struct would need `T: ToJsonValue<V>`-style bounds on the generated
29//! `impl`, which the derive cannot express yet. Deriving on a generic struct
30//! fails with unresolved trait-bound errors; use concrete field types.
31
32// On docs.rs, label every feature-gated item with the feature that unlocks it.
33// `doc(auto_cfg)` is still nightly-gated, and `docsrs` is set by docs.rs itself
34// (see `rustdoc-args` in Cargo.toml), so stable builds are unaffected.
35#![cfg_attr(docsrs, feature(doc_cfg))]
36#![cfg_attr(docsrs, doc(auto_cfg))]
37#[cfg(feature = "codegen")]
38#[doc(hidden)]
39pub mod attrs;
40#[cfg(feature = "codegen")]
41#[doc(hidden)]
42pub mod codegen;
43#[cfg(feature = "codegen")]
44#[doc(hidden)]
45pub mod ir;
46#[cfg(feature = "codegen")]
47#[doc(hidden)]
48pub mod iri;
49
50mod value;
51
52#[cfg(any(feature = "serde-json", feature = "sonic-rs", feature = "jstrict"))]
53mod backends;
54
55#[cfg(feature = "chrono")]
56mod interop_chrono;
57
58pub use value::{JsonValue, ToJsonValue};
59
60/// Convert `&self` into the expanded JSON-LD form for the chosen backend `V`.
61///
62/// Generated by the `Expandable` derive in `jsonld-expandable`.
63pub trait Expandable {
64    /// Render `self` as expanded JSON-LD using backend `V`.
65    fn expand<V: JsonValue>(&self) -> V;
66}
67
68/// Field marker for dynamic `@type` arrays.
69///
70/// Implement this for fields tagged with `#[jsonld(type_value)]` to provide a
71/// `@type` array whose contents are computed at runtime.
72pub trait ExpandableTypeValue {
73    /// Renders the node's `@type` entry as a JSON array of type IRIs in the
74    /// chosen backend `V`.
75    fn to_type_array<V: JsonValue>(&self) -> V;
76}
77
78/// Renders a field tagged `#[jsonld(container = "language")]`.
79///
80/// Implemented here for `HashMap` and `BTreeMap` whose keys and values are both
81/// `AsRef<str>`, which covers the ordinary language map of tag to text. A
82/// language that carries several strings, or values that are not strings, needs
83/// its own implementation.
84pub trait ExpandableLanguageMap {
85    /// Renders the map in expanded language-map form: a JSON array of
86    /// `{"@value": ..., "@language": ...}` objects in the chosen backend `V`.
87    fn to_expanded_language_map<V: JsonValue>(&self) -> V;
88}
89
90fn language_entries<'a, V: JsonValue, I>(entries: I) -> V
91where
92    I: IntoIterator<Item = (&'a str, &'a str)>,
93{
94    V::array(
95        entries
96            .into_iter()
97            .map(|(lang, text)| V::object([("@value".to_string(), V::string(text)), ("@language".to_string(), V::string(lang))])),
98    )
99}
100
101impl<K: AsRef<str>, T: AsRef<str>, S> ExpandableLanguageMap for std::collections::HashMap<K, T, S> {
102    fn to_expanded_language_map<V: JsonValue>(&self) -> V {
103        // `HashMap` iteration order varies between processes. Sorting by
104        // language tag keeps the emitted array stable, which matters as soon as
105        // the output is compared, hashed or golden-tested.
106        let mut entries: Vec<(&str, &str)> = self.iter().map(|(k, v)| (k.as_ref(), v.as_ref())).collect();
107        entries.sort_unstable_by_key(|(lang, _)| *lang);
108        language_entries(entries)
109    }
110}
111
112impl<K: AsRef<str>, T: AsRef<str>> ExpandableLanguageMap for std::collections::BTreeMap<K, T> {
113    fn to_expanded_language_map<V: JsonValue>(&self) -> V {
114        language_entries(self.iter().map(|(k, v)| (k.as_ref(), v.as_ref())))
115    }
116}
117
118/// Driver entry used by the proc-macro shell crate.
119///
120/// Lives here so the parser, IR, and codegen can be exercised by ordinary
121/// `#[test]` functions; proc-macro crates cannot host normal tests.
122///
123/// `runtime` is the path the generated code uses to reach this crate. Only the
124/// derive can determine it, since it depends on how the *consuming* crate
125/// depends on this one.
126#[cfg(feature = "codegen")]
127#[doc(hidden)]
128#[must_use]
129pub fn derive_expandable(input: &syn::DeriveInput, runtime: &proc_macro2::TokenStream) -> proc_macro2::TokenStream {
130    match codegen::generate(input, runtime) {
131        Ok(ts) => ts,
132        Err(err) => err.to_compile_error(),
133    }
134}