Skip to main content

yo_derive/
lib.rs

1//! `#[derive(Yo)]`, which writes a type's shape, its document encoding and the
2//! indexes it declares (`15` sections 3 and 4).
3//!
4//! ```ignore
5//! #[derive(Yo)]
6//! struct Order {
7//!     #[yo(id)]
8//!     id: u64,
9//!     #[yo(index)]
10//!     status: String,
11//!     #[yo(ordered)]
12//!     total: f64,
13//!     #[yo(array)]
14//!     tags: Vec<String>,
15//!     #[yo(text)]
16//!     note: String,
17//! }
18//! ```
19//!
20//! That gives `Order` three things. A [shape], which is the canonical
21//! description the collection is created with and every later open is checked
22//! against. A document encoding, so a value goes into the store as YOJB without
23//! passing through JSON text. And a list of indexes, which the collection
24//! declares the first time it is opened, plus a `Path` constant per indexed
25//! field so a query is written `Order::STATUS` rather than `"$.status"`.
26//!
27//! # The words
28//!
29//! `#[yo(id)]` names the field that is the document's id, and a type needs one
30//! to be a document at all. `#[yo(index)]` asks equality, `#[yo(ordered)]` asks
31//! equality and ranges, `#[yo(array)]` files the document under every element
32//! of a list, and `#[yo(text)]` files it under every word of a string. They are
33//! the four kinds a path index comes in and nothing is invented here.
34//!
35//! # Why there are no dependencies
36//!
37//! A derive is the one place a library gets to put three crates in everybody's
38//! build graph without being asked, and the usual three are most of what a
39//! cold build of a small program costs. What this reads is a struct with named
40//! fields, and a field is an attribute, a visibility, a name, a colon and some
41//! tokens. The compiler hands that over already split into tokens, so the
42//! parser in `parse` is a few hundred lines and the types themselves are
43//! carried straight back out without ever being understood.
44//!
45//! The cost of that choice is that the errors here are sentences rather than
46//! spans, so a mistake points at the struct rather than at the word. That is
47//! the trade, and it is written down rather than discovered.
48//!
49//! [shape]: https://docs.rs/yodb/latest/yo/trait.Shape.html
50
51#![deny(missing_docs)]
52
53mod parse;
54
55use proc_macro::TokenStream;
56
57use parse::{Field, Struct};
58
59/// Write a type's shape, its document encoding and its indexes.
60///
61/// See the module docs for what the attributes mean.
62#[proc_macro_derive(Yo, attributes(yo))]
63pub fn yo(input: TokenStream) -> TokenStream {
64    match parse::parse(input).and_then(emit) {
65        Ok(out) => out,
66        Err(why) => complain(&why),
67    }
68}
69
70/// Hand a sentence back to the compiler instead of code.
71fn complain(why: &str) -> TokenStream {
72    format!("::core::compile_error!{{ {why:?} }}")
73        .parse()
74        .expect("a compile_error with a string in it")
75}
76
77fn emit(s: Struct) -> Result<TokenStream, String> {
78    let name = &s.name;
79    let mut out = String::new();
80
81    out.push_str(&shape(&s));
82    out.push_str(&field(&s));
83    out.push_str(&indexed(&s));
84    if let Some(id) = the_id(&s)? {
85        out.push_str(&document(&s, id));
86    }
87    out.push_str(&paths(&s));
88
89    out.parse().map_err(|e| {
90        format!("Yo wrote something the compiler would not take for {name}, which is a bug in the derive: {e}")
91    })
92}
93
94/// The one field marked `#[yo(id)]`, if there is one.
95fn the_id(s: &Struct) -> Result<Option<&Field>, String> {
96    let mut marked = s.fields.iter().filter(|f| f.id);
97    let Some(first) = marked.next() else {
98        return Ok(None);
99    };
100    if let Some(second) = marked.next() {
101        return Err(format!(
102            "{} marks both {} and {} as its id, and a document is stored under one",
103            s.name, first.label, second.label
104        ));
105    }
106    Ok(Some(first))
107}
108
109fn shape(s: &Struct) -> String {
110    let mut fields = String::new();
111    for f in &s.fields {
112        let (label, ty) = (&f.label, &f.ty);
113        fields.push_str(&format!("({label:?}, <{ty} as ::yo::Shape>::describe),"));
114    }
115    let (name, label) = (&s.name, &s.name);
116    format!(
117        "#[automatically_derived]
118impl ::yo::Shape for {name} {{
119    fn describe(d: &mut ::yo::Desc) {{
120        d.strukt({label:?}, &[{fields}]);
121    }}
122}}
123"
124    )
125}
126
127fn field(s: &Struct) -> String {
128    let mut write = String::new();
129    let mut read = String::new();
130    for f in &s.fields {
131        let (name, label) = (&f.name, &f.label);
132        write.push_str(&format!(
133            "b.key({label:?}.as_bytes())?; ::yo::doc::Field::write(&self.{name}, b)?;"
134        ));
135        read.push_str(&format!("{name}: ::yo::doc::at(d, {label:?})?,"));
136    }
137    let name = &s.name;
138    format!(
139        "#[automatically_derived]
140impl ::yo::doc::Field for {name} {{
141    fn write(&self, b: &mut ::yo::doc::Builder) -> ::yo::Result<()> {{
142        b.begin_object()?;
143        {write}
144        b.end_object()
145    }}
146
147    fn read(d: ::yo::doc::Doc<'_>) -> ::yo::Result<{name}> {{
148        ::yo::doc::expect_object(d, {name:?})?;
149        Ok({name} {{ {read} }})
150    }}
151}}
152"
153    )
154}
155
156/// The indexes a type declares, which every derived type has whether or not it
157/// has an id. An edge type has no id and still declares indexes, so this is a
158/// trait of its own rather than a constant on `Document`.
159fn indexed(s: &Struct) -> String {
160    let mut indexes = String::new();
161    for f in &s.fields {
162        if let Some(kind) = f.kind {
163            let path = format!("$.{}", f.label);
164            indexes.push_str(&format!("({path:?}, ::yo::doc::IndexKind::{kind}),"));
165        }
166    }
167    let name = &s.name;
168    format!(
169        "#[automatically_derived]
170impl ::yo::doc::Indexed for {name} {{
171    const INDEXES: &'static [(&'static str, ::yo::doc::IndexKind)] = &[{indexes}];
172}}
173"
174    )
175}
176
177fn document(s: &Struct, id: &Field) -> String {
178    let (name, ty, at) = (&s.name, &id.ty, &id.name);
179    format!(
180        "#[automatically_derived]
181impl ::yo::doc::Document for {name} {{
182    type Id = {ty};
183
184    fn id(&self) -> &{ty} {{
185        &self.{at}
186    }}
187}}
188"
189    )
190}
191
192/// A `Path` constant per indexed field, so a query names the field rather than
193/// a string that the compiler cannot check.
194fn paths(s: &Struct) -> String {
195    let mut consts = String::new();
196    for f in &s.fields {
197        let Some(kind) = f.kind else { continue };
198        // The key of an array index is one element, so that is what a query
199        // against it compares. Everything else queries its own type.
200        let asked = match (kind, &f.elem) {
201            ("Array", Some(elem)) => elem,
202            _ => &f.ty,
203        };
204        let (upper, label, path) = (f.label.to_uppercase(), &f.label, format!("$.{}", f.label));
205        let name = &s.name;
206        // An ordered index answers ranges as well, and that is a different type
207        // rather than a flag, so that a range over an equality index does not
208        // compile.
209        let held = if kind == "Ordered" {
210            format!("::yo::doc::Ordered<{name}, {asked}>")
211        } else {
212            format!("::yo::doc::Path<{name}, {asked}>")
213        };
214        let built = if kind == "Ordered" {
215            format!("::yo::doc::Ordered::new({path:?})")
216        } else {
217            format!("::yo::doc::Path::new({path:?}, ::yo::doc::IndexKind::{kind})")
218        };
219        consts.push_str(&format!(
220            "    /// The `{path}` path, which is indexed for {kind} and named
221    /// `{label}` on this type.
222    pub const {upper}: {held} = {built};
223"
224        ));
225    }
226    if consts.is_empty() {
227        return String::new();
228    }
229    let name = &s.name;
230    format!("impl {name} {{\n{consts}}}\n")
231}