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