Skip to main content

jsonld_expandable/
lib.rs

1//! Derive macro generating expanded JSON-LD directly from Rust types.
2//!
3//! All parsing, IR, and codegen logic lives in `jsonld-expandable-core` so
4//! it can be unit-tested without `trybuild`.
5
6use proc_macro::TokenStream;
7use proc_macro_crate::{FoundCrate, crate_name};
8use proc_macro2::TokenStream as TokenStream2;
9use quote::{format_ident, quote};
10use syn::{DeriveInput, parse_macro_input};
11
12/// Resolves the path the generated code uses to reach the runtime crate.
13///
14/// The generated `impl` is compiled inside the consuming crate, so the path has
15/// to name a crate that crate depends on. That is either
16/// `jsonld-expandable-core` directly, giving `::jsonld_expandable_core`, or the
17/// umbrella `jsonld` crate with an `expandable*` feature, giving
18/// `::jsonld::expandable_core`. Either may be renamed in `Cargo.toml`.
19fn runtime_path(span: proc_macro2::Span) -> syn::Result<TokenStream2> {
20    if let Ok(found) = crate_name("jsonld-expandable-core") {
21        return Ok(match found {
22            FoundCrate::Itself => quote!(crate),
23            FoundCrate::Name(name) => {
24                let ident = format_ident!("{}", name);
25                quote!(::#ident)
26            }
27        });
28    }
29
30    match crate_name("jsonld") {
31        Ok(FoundCrate::Itself) => Ok(quote!(crate::expandable_core)),
32        Ok(FoundCrate::Name(name)) => {
33            let ident = format_ident!("{}", name);
34            Ok(quote!(::#ident::expandable_core))
35        }
36        Err(_) => Err(syn::Error::new(
37            span,
38            "`Expandable` needs its runtime crate as a dependency: add `jsonld` with an \
39             `expandable` feature, or `jsonld-expandable-core` directly",
40        )),
41    }
42}
43
44/// Derive `Expandable` to generate an `expand::<V>()` impl producing expanded
45/// JSON-LD for any `V: JsonValue` backend.
46///
47/// ```ignore
48/// // The derive macro, plus the trait it implements: `expand()` is a trait
49/// // method, so both names must be in scope. Deriving through the umbrella
50/// // `jsonld` crate, a single `use jsonld::Expandable;` brings both.
51/// use jsonld_expandable::Expandable;
52/// use jsonld_expandable_core::Expandable as _;
53///
54/// #[derive(Expandable)]
55/// #[jsonld(type = "https://example.com/Parent")]
56/// pub struct Parent {
57///     #[jsonld(id)]
58///     pub id: String,
59///
60///     #[jsonld(property = "https://example.com/name")]
61///     pub name: String,
62/// }
63///
64/// let expanded: serde_json::Value = parent.expand();
65/// ```
66///
67/// # Container attributes
68///
69/// Set on the struct itself, inside `#[jsonld(...)]`:
70///
71/// | Attribute | Meaning |
72/// |---|---|
73/// | `type = "IRI-or-CURIE"` | Static `@type` for every expanded node. |
74/// | `fragment` | Emit the object without `@type` (a sub-fragment of a parent node). Mutually exclusive with `type` and a `type_value` field. |
75/// | `prefix(name = "IRI", ...)` | Declare CURIE prefixes used by `type`, `property`, and `coerce` values. Hyphenated names work bare (`prefix(ngsi-ld = "...")`) or quoted (`prefix("ngsi-ld" = "...")`). |
76/// | `debug` | Report the generated code as a compile error (for inspection). |
77///
78/// Exactly one of `type = "..."`, a `type_value` field, or `fragment` is
79/// required.
80///
81/// Each struct carries its own prefix table, and the derive sees one struct at a
82/// time: a nested or fragment struct needs its own `prefix(...)` even when its
83/// parent declares the same names. A prefix that is not declared is not an
84/// error, because `ex:name` is also a syntactically valid IRI with scheme `ex`,
85/// so it is emitted as written.
86///
87/// The generated code reaches its runtime support through
88/// `jsonld-expandable-core` or through the umbrella `jsonld` crate's re-export
89/// of it, whichever the consuming crate depends on, following a renamed
90/// dependency to the name it was given.
91///
92/// # Field attributes
93///
94/// | Attribute | Meaning |
95/// |---|---|
96/// | `id` | Field is the node's `@id`. `String`-like (`AsRef<str>`) or `Option` thereof; `None` omits the `@id` entry. |
97/// | `type_value` | Field supplies a dynamic `@type` array via the `ExpandableTypeValue` trait. At most one per struct; conflicts with container `type`/`fragment`. |
98/// | `property = "IRI-or-CURIE"` | Property IRI the field expands under. Required unless the field is `id`, `skip`, `flatten`, or `flatten_map`. |
99/// | `skip` | Exclude the field from expansion. |
100/// | `coerce = "@id"` / `"@vocab"` | Value is an IRI reference: emits `[{"@id": v}]` (or `@vocab`). With `vec`, iterates the field. |
101/// | `coerce = "@json"` | JSON literal: emits `[{"@value": v, "@type": "@json"}]`. |
102/// | `coerce = "IRI-or-CURIE"` | Typed literal: emits `[{"@value": v, "@type": "<datatype>"}]`. CURIEs expand through this struct's own `prefix(...)` table. |
103/// | `container = "list"` | Wrap values as `[{"@list": [...]}]`. Composes with `coerce` and `nested` for per-item shapes. |
104/// | `container = "set"` | Emit the value's `ToJsonValue` form verbatim (JSON array). |
105/// | `container = "language"` | Language map: `HashMap`/`BTreeMap` of tag to text work as they are, other shapes implement `ExpandableLanguageMap`. |
106/// | `container = "index"` | Index map: emits `[{"@index": k, "@value": v}, ...]` from an iterable of pairs. |
107/// | `nested` | Field is itself `Expandable`; recurse. With `vec`, expands each element. |
108/// | `vec` | Field is a `Vec`-like iterable; combine with `nested` or an `@id`/`@vocab` coercion. |
109/// | `flatten` | Merge the expanded properties of a nested `Expandable` into this node (drops its `@id`/`@type`). |
110/// | `flatten_map` | Merge a map field: each key becomes a property IRI, each value is expanded recursively. |
111/// | `passthrough` | The field's own `Expandable` impl produces the final JSON-LD form; emitted verbatim. |
112///
113/// `Option<T>` fields omit their entry when `None`.
114///
115/// # Limitations
116///
117/// Generic structs are not supported: the derive cannot yet add the
118/// `T: ToJsonValue<V>`-style bounds the generated impl would need. Reserved
119/// attributes `nest`, `reverse`, and `container = "id"/"type"/"graph"` fail
120/// with an explicit error.
121#[proc_macro_derive(Expandable, attributes(jsonld))]
122pub fn derive_expandable(input: TokenStream) -> TokenStream {
123    let ast = parse_macro_input!(input as DeriveInput);
124    match runtime_path(ast.ident.span()) {
125        Ok(runtime) => jsonld_expandable_core::derive_expandable(&ast, &runtime).into(),
126        Err(err) => err.to_compile_error().into(),
127    }
128}