Skip to main content

nextjson_derive/
lib.rs

1//! Zero-dependency derive macros for `nextjson`.
2//!
3//! Implemented entirely with the standard `proc_macro` API: no `syn`, no
4//! `quote`, no `proc-macro2`. The input `TokenStream` is parsed by a
5//! hand-written recursive-descent parser into a small AST, and the output is
6//! generated as text and re-parsed.
7
8#![deny(unsafe_code)]
9#![deny(missing_docs)]
10#![doc(html_root_url = "https://docs.rs/nextjson-derive")]
11
12extern crate proc_macro;
13
14use proc_macro::{Delimiter, Ident, Spacing, TokenStream, TokenTree};
15use std::str::FromStr;
16
17mod attr;
18mod case;
19mod de;
20mod schema;
21mod ser;
22
23pub(crate) use attr::{ContainerAttrs, FieldAttrs, VariantAttrs};
24
25/// Parse a string into a TokenStream.
26pub(crate) fn ts(s: &str) -> TokenStream {
27    TokenStream::from_str(s)
28        .unwrap_or_else(|e| panic!("nextjson-derive: invalid generated tokens: {e:?}"))
29}
30
31/// Build a `compile_error!` expansion from a message.
32pub(crate) fn err(msg: &str) -> TokenStream {
33    ts(&format!("::core::compile_error!({:?})", msg))
34}
35
36/// Error string returned by codegen helpers.
37pub(crate) fn err_str(msg: &str) -> String {
38    msg.to_string()
39}
40
41/// Token cursor over a slice of TokenTrees.
42pub(crate) struct P<'a> {
43    pub toks: &'a [TokenTree],
44    pub i: usize,
45}
46
47impl<'a> P<'a> {
48    pub fn peek(&self) -> Option<&TokenTree> {
49        self.toks.get(self.i)
50    }
51    pub fn next(&mut self) -> Option<TokenTree> {
52        let t = self.toks.get(self.i).cloned();
53        if t.is_some() {
54            self.i += 1;
55        }
56        t
57    }
58    pub fn is_ident(&self, s: &str) -> bool {
59        matches!(self.peek(), Some(TokenTree::Ident(id)) if id.to_string() == s)
60    }
61    pub fn is_punct(&self, ch: char) -> bool {
62        matches!(self.peek(), Some(TokenTree::Punct(p)) if p.as_char() == ch)
63    }
64    pub fn eat_ident(&mut self, s: &str) -> bool {
65        if self.is_ident(s) {
66            self.i += 1;
67            true
68        } else {
69            false
70        }
71    }
72    pub fn eat_punct(&mut self, ch: char) -> bool {
73        if self.is_punct(ch) {
74            self.i += 1;
75            true
76        } else {
77            false
78        }
79    }
80    pub fn expect_ident(&mut self) -> Option<String> {
81        match self.next() {
82            Some(TokenTree::Ident(id)) => Some(id.to_string()),
83            _ => None,
84        }
85    }
86}
87
88/// Join tokens into a re-parseable string, preserving `Joint` spacing so
89/// that punctuation sequences (`::`, `'a`, `->`, `>>`) stay adjacent.
90pub(crate) fn join(toks: &[TokenTree]) -> String {
91    let mut s = String::new();
92    let mut no_space = false;
93    for t in toks {
94        if !s.is_empty() && !no_space {
95            s.push(' ');
96        }
97        no_space = false;
98        match t {
99            TokenTree::Punct(p) => {
100                s.push_str(&p.to_string());
101                no_space = p.spacing() == Spacing::Joint;
102            }
103            _ => s.push_str(&t.to_string()),
104        }
105    }
106    s
107}
108
109/// Split tokens at a top-level separator.
110///
111/// Angle brackets are tracked so that generic types such as
112/// `BTreeMap<String, i32>` stay on a single side of the split.
113pub(crate) fn split_top(toks: &[TokenTree], sep: char) -> Vec<Vec<TokenTree>> {
114    let mut out: Vec<Vec<TokenTree>> = Vec::new();
115    let mut cur: Vec<TokenTree> = Vec::new();
116    let mut angle: usize = 0;
117    for tt in toks {
118        match tt {
119            TokenTree::Group(_) => cur.push(tt.clone()),
120            TokenTree::Punct(p) if p.as_char() == '<' => {
121                angle += 1;
122                cur.push(tt.clone());
123            }
124            TokenTree::Punct(p) if p.as_char() == '>' => {
125                angle = angle.saturating_sub(1);
126                cur.push(tt.clone());
127            }
128            TokenTree::Punct(p) if angle == 0 && p.as_char() == sep => {
129                out.push(std::mem::take(&mut cur));
130            }
131            _ => cur.push(tt.clone()),
132        }
133    }
134    if !cur.is_empty() {
135        out.push(cur);
136    }
137    if out.is_empty() {
138        out.push(Vec::new());
139    }
140    out
141}
142
143/// Read a `<...>` group. proc_macro does not group angle brackets, so this
144/// scans for the matching `>` while ignoring `->` arrow tokens.
145pub(crate) fn read_angle(p: &mut P) -> Option<Vec<TokenTree>> {
146    if !p.eat_punct('<') {
147        return None;
148    }
149    let mut depth = 1usize;
150    let mut out = Vec::new();
151    while let Some(tt) = p.next() {
152        match &tt {
153            TokenTree::Punct(c) if c.as_char() == '<' => {
154                depth += 1;
155                out.push(tt);
156            }
157            TokenTree::Punct(c)
158                if c.as_char() == '-'
159                    && matches!(p.peek(), Some(TokenTree::Punct(n)) if n.as_char() == '>') =>
160            {
161                out.push(tt);
162                out.push(p.next().unwrap());
163            }
164            TokenTree::Punct(c) if c.as_char() == '>' => {
165                if depth == 1 {
166                    return Some(out);
167                }
168                depth -= 1;
169                out.push(tt);
170            }
171            _ => out.push(tt),
172        }
173    }
174    None
175}
176
177// ---------------------------------------------------------------------------
178// AST
179// ---------------------------------------------------------------------------
180
181#[derive(Clone, Copy, PartialEq, Eq)]
182pub(crate) enum ParamKind {
183    Lifetime,
184    Type,
185    Const,
186}
187
188#[derive(Clone)]
189pub(crate) struct GenericParam {
190    pub kind: ParamKind,
191    pub full: String,
192    pub name: String,
193}
194
195#[derive(Clone, Default)]
196pub(crate) struct Generics {
197    pub params: Vec<GenericParam>,
198    pub where_preds: Vec<String>,
199}
200
201#[derive(Clone)]
202pub(crate) struct Field {
203    pub ident: Option<String>,
204    pub ty: String,
205    pub attrs: Vec<attr::Meta>,
206}
207
208#[derive(Clone)]
209pub(crate) enum Fields {
210    Unit,
211    Named(Vec<Field>),
212    Unnamed(Vec<Field>),
213}
214
215impl Fields {
216    pub fn iter(&self) -> core::slice::Iter<'_, Field> {
217        match self {
218            Fields::Unit => [].iter(),
219            Fields::Named(fields) | Fields::Unnamed(fields) => fields.iter(),
220        }
221    }
222}
223
224#[derive(Clone)]
225pub(crate) struct Variant {
226    pub ident: String,
227    pub fields: Fields,
228    pub attrs: Vec<attr::Meta>,
229}
230
231#[derive(Clone)]
232pub(crate) enum Data {
233    Struct(Fields),
234    Enum(Vec<Variant>),
235}
236
237#[derive(Clone)]
238pub(crate) struct Input {
239    pub ident: String,
240    pub generics: Generics,
241    pub data: Data,
242    pub cattr: ContainerAttrs,
243}
244
245// ---------------------------------------------------------------------------
246// Attribute collection
247// ---------------------------------------------------------------------------
248
249/// Collect leading `#[...]` attribute groups.
250fn parse_attrs(p: &mut P) -> Vec<Vec<TokenTree>> {
251    let mut out = Vec::new();
252    while p.is_punct('#') {
253        p.next();
254        if let Some(TokenTree::Group(g)) = p.next() {
255            if g.delimiter() == Delimiter::Bracket {
256                out.push(g.stream().into_iter().collect());
257            }
258        }
259    }
260    out
261}
262
263/// Extract `njson` / `nextjson` metas from a set of attribute groups.
264fn collect_metas(groups: &[Vec<TokenTree>]) -> Vec<attr::Meta> {
265    let mut out = Vec::new();
266    for g in groups {
267        out.extend(attr::metas_from_attr(g));
268    }
269    out
270}
271
272// ---------------------------------------------------------------------------
273// Top-level parse
274// ---------------------------------------------------------------------------
275
276pub(crate) fn parse_input(input: TokenStream) -> Result<Input, String> {
277    let toks: Vec<TokenTree> = input.into_iter().collect();
278    let mut p = P { toks: &toks, i: 0 };
279
280    let attrs = parse_attrs(&mut p);
281    let cattr = ContainerAttrs::from_metas(&collect_metas(&attrs));
282    eat_visibility(&mut p);
283
284    let is_enum = if p.eat_ident("struct") {
285        false
286    } else if p.eat_ident("enum") {
287        true
288    } else {
289        return Err("nextjson: expected `struct` or `enum`".into());
290    };
291
292    let ident = p
293        .expect_ident()
294        .ok_or_else(|| "nextjson: expected type name".to_string())?;
295
296    let mut generics = Generics::default();
297    if let Some(inner) = read_angle(&mut p) {
298        generics = parse_generics(&inner);
299    }
300
301    let data = if !is_enum
302        && matches!(p.peek(), Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Parenthesis)
303    {
304        let Some(TokenTree::Group(body)) = p.next() else {
305            return Err("nextjson: expected a tuple struct body".into());
306        };
307        if p.eat_ident("where") {
308            parse_where_clause(&mut p, &mut generics, false);
309        }
310        let inner: Vec<TokenTree> = body.stream().into_iter().collect();
311        Data::Struct(Fields::Unnamed(parse_unnamed_fields(&inner)))
312    } else {
313        if p.eat_ident("where") {
314            parse_where_clause(&mut p, &mut generics, true);
315        }
316        if !is_enum {
317            match p.next() {
318                Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace => {
319                    let inner: Vec<TokenTree> = g.stream().into_iter().collect();
320                    Data::Struct(Fields::Named(parse_named_fields(&inner)))
321                }
322                Some(TokenTree::Punct(pc)) if pc.as_char() == ';' => Data::Struct(Fields::Unit),
323                _ => return Err("nextjson: expected a struct body".into()),
324            }
325        } else {
326            match p.next() {
327                Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace => {
328                    let inner: Vec<TokenTree> = g.stream().into_iter().collect();
329                    Data::Enum(parse_variants(&inner))
330                }
331                _ => return Err("nextjson: expected an enum body".into()),
332            }
333        }
334    };
335
336    Ok(Input {
337        ident,
338        generics,
339        data,
340        cattr,
341    })
342}
343
344fn parse_where_clause(p: &mut P<'_>, generics: &mut Generics, has_braced_body: bool) {
345    let mut tokens = Vec::new();
346    while let Some(token) = p.peek() {
347        let is_body = has_braced_body
348            && p.i + 1 == p.toks.len()
349            && matches!(token, TokenTree::Group(g) if g.delimiter() == Delimiter::Brace);
350        if is_body || matches!(token, TokenTree::Punct(punct) if punct.as_char() == ';') {
351            break;
352        }
353        if let Some(token) = p.next() {
354            tokens.push(token);
355        }
356    }
357    for piece in split_top(&tokens, ',') {
358        let predicate = join(&piece).trim().to_string();
359        if !predicate.is_empty() {
360            generics.where_preds.push(predicate);
361        }
362    }
363}
364
365fn parse_generics(inner: &[TokenTree]) -> Generics {
366    let mut g = Generics::default();
367    for item in split_top(inner, ',') {
368        if item.is_empty() {
369            continue;
370        }
371        let declaration = strip_generic_default(&item);
372        let mut p = P {
373            toks: &declaration,
374            i: 0,
375        };
376        if p.is_punct('\'') {
377            p.next();
378            let name = p.expect_ident().unwrap_or_default();
379            g.params.push(GenericParam {
380                kind: ParamKind::Lifetime,
381                full: join(&declaration),
382                name: format!("'{name}"),
383            });
384        } else if p.eat_ident("const") {
385            let name = p.expect_ident().unwrap_or_default();
386            g.params.push(GenericParam {
387                kind: ParamKind::Const,
388                full: join(&declaration),
389                name,
390            });
391        } else {
392            let name = p.expect_ident().unwrap_or_default();
393            g.params.push(GenericParam {
394                kind: ParamKind::Type,
395                full: join(&declaration),
396                name,
397            });
398        }
399    }
400    g
401}
402
403fn strip_generic_default(tokens: &[TokenTree]) -> Vec<TokenTree> {
404    let mut angle_depth = 0usize;
405    for (index, token) in tokens.iter().enumerate() {
406        match token {
407            TokenTree::Punct(punct) if punct.as_char() == '<' => angle_depth += 1,
408            TokenTree::Punct(punct) if punct.as_char() == '>' => {
409                angle_depth = angle_depth.saturating_sub(1);
410            }
411            TokenTree::Punct(punct) if punct.as_char() == '=' && angle_depth == 0 => {
412                return tokens[..index].to_vec();
413            }
414            _ => {}
415        }
416    }
417    tokens.to_vec()
418}
419
420fn parse_named_fields(inner: &[TokenTree]) -> Vec<Field> {
421    split_top(inner, ',')
422        .iter()
423        .filter(|s| !s.is_empty())
424        .map(|piece| parse_named_field(piece))
425        .collect()
426}
427
428/// Consume an optional `pub` visibility specifier (`pub`, `pub(crate)`,
429/// `pub(super)`, `pub(in path)`). In the proc-macro token stream the
430/// parenthesized part arrives as a `Group` with `Parenthesis` delimiter, not
431/// as a `Punct('(')`, so it must be matched as a group.
432pub(crate) fn eat_visibility(p: &mut P<'_>) {
433    if !p.eat_ident("pub") {
434        return;
435    }
436    if matches!(p.peek(), Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Parenthesis) {
437        p.next();
438    }
439}
440
441fn parse_named_field(piece: &[TokenTree]) -> Field {
442    let mut p = P { toks: piece, i: 0 };
443    let attrs = parse_attrs(&mut p);
444    eat_visibility(&mut p);
445    // Find the field separator ':' at top level, excluding '::'.
446    let mut colon = None;
447    let mut j = p.i;
448    while j < piece.len() {
449        match &piece[j] {
450            TokenTree::Punct(c) if c.as_char() == ':' => {
451                if matches!(piece.get(j + 1), Some(TokenTree::Punct(n)) if n.as_char() == ':') {
452                    j += 2;
453                    continue;
454                }
455                colon = Some(j);
456                break;
457            }
458            _ => j += 1,
459        }
460    }
461    match colon {
462        Some(c) => Field {
463            ident: Some(join(&piece[p.i..c]).trim().to_string()),
464            ty: join(&piece[c + 1..]).trim().to_string(),
465            attrs: collect_metas(&attrs),
466        },
467        None => Field {
468            ident: None,
469            ty: join(&piece[p.i..]).trim().to_string(),
470            attrs: collect_metas(&attrs),
471        },
472    }
473}
474
475fn parse_unnamed_fields(inner: &[TokenTree]) -> Vec<Field> {
476    split_top(inner, ',')
477        .iter()
478        .filter(|s| !s.is_empty())
479        .map(|piece| {
480            let mut p = P { toks: piece, i: 0 };
481            let attrs = parse_attrs(&mut p);
482            eat_visibility(&mut p);
483            Field {
484                ident: None,
485                ty: join(&piece[p.i..]).trim().to_string(),
486                attrs: collect_metas(&attrs),
487            }
488        })
489        .collect()
490}
491
492fn parse_variants(inner: &[TokenTree]) -> Vec<Variant> {
493    split_top(inner, ',')
494        .iter()
495        .filter(|s| !s.is_empty())
496        .map(|piece| {
497            let mut p = P { toks: piece, i: 0 };
498            let attrs = parse_attrs(&mut p);
499            let ident = p.expect_ident().unwrap_or_default();
500            let fields = match p.next() {
501                Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace => {
502                    let inner2: Vec<TokenTree> = g.stream().into_iter().collect();
503                    Fields::Named(parse_named_fields(&inner2))
504                }
505                Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Parenthesis => {
506                    let inner2: Vec<TokenTree> = g.stream().into_iter().collect();
507                    Fields::Unnamed(parse_unnamed_fields(&inner2))
508                }
509                _ => Fields::Unit,
510            };
511            Variant {
512                ident,
513                fields,
514                attrs: collect_metas(&attrs),
515            }
516        })
517        .collect()
518}
519
520// ---------------------------------------------------------------------------
521// Generic helpers for code generation
522// ---------------------------------------------------------------------------
523
524/// Build `(impl_generics, ty_generics, where_clause)` for the impl header.
525pub(crate) fn build_generics(
526    input: &Input,
527    cp: &str,
528    de: bool,
529    has_flatten: bool,
530    has_borrow: bool,
531) -> (String, String, String) {
532    let g = &input.generics;
533    let c = &input.cattr;
534
535    let mut impl_params: Vec<String> = g.params.iter().map(|p| p.full.clone()).collect();
536    if de {
537        impl_params.insert(0, "'de".to_string());
538    }
539    let impl_generics = if impl_params.is_empty() {
540        String::new()
541    } else {
542        format!("<{}>", impl_params.join(", "))
543    };
544
545    let names: Vec<String> = g.params.iter().map(|p| p.name.clone()).collect();
546    let ty_generics = if names.is_empty() {
547        String::new()
548    } else {
549        format!("<{}>", names.join(", "))
550    };
551
552    let mut preds: Vec<String> = if let Some(bound) = &c.bound {
553        let cleaned = bound.trim().trim_matches('"');
554        if cleaned.is_empty() {
555            Vec::new()
556        } else {
557            cleaned
558                .split(',')
559                .map(|s| s.trim().to_string())
560                .filter(|s| !s.is_empty())
561                .collect()
562        }
563    } else {
564        let mut v: Vec<String> = g.where_preds.clone();
565        for p in g.params.iter() {
566            if p.kind != ParamKind::Type {
567                continue;
568            }
569            if de && has_flatten {
570                v.push(format!(
571                    "{0}: for<'__n> {1}::NsonDeserialize<'__n>",
572                    p.name, cp
573                ));
574            } else if de {
575                v.push(format!("{}: {}::NsonDeserialize<'de>", p.name, cp));
576            } else {
577                v.push(format!("{}: {}::NsonSerialize", p.name, cp));
578            }
579        }
580        v
581    };
582
583    if de && has_borrow {
584        for p in g.params.iter() {
585            if p.kind == ParamKind::Lifetime {
586                preds.push(format!("'de: {}", p.name));
587            }
588        }
589    }
590
591    let where_clause = if preds.is_empty() {
592        String::new()
593    } else {
594        format!(" where {}", preds.join(", "))
595    };
596
597    (impl_generics, ty_generics, where_clause)
598}
599
600/// Emit the `NsonSchema` + `NsonSerialize` impls.
601pub(crate) fn generate_impls(input: &Input) -> TokenStream {
602    let cp = input.cattr.crate_path.clone();
603    let name = input.ident.clone();
604    let (ig, tg, wc) = build_generics(input, &cp, false, false, false);
605    let schema_expr = schema::schema_expr(input, &cp);
606    let body = match &input.data {
607        Data::Struct(f) => ser::serialize_struct(&name, f, input, &cp),
608        Data::Enum(v) => ser::serialize_enum(&name, v, input, &cp),
609    };
610    let out = format!(
611        "#[automatically_derived]\n\
612         impl {ig} {cp}::NsonSchema for {name}{tg}{wc} {{\n\
613         \x20   const SCHEMA: {cp}::TypeSchema = {schema_expr};\n\
614         }}\n\
615         #[automatically_derived]\n\
616         impl {ig} {cp}::NsonSerialize for {name}{tg}{wc} {{\n\
617         \x20   fn nextencode<__E: {cp}::FormatEncoder>(&self, __e: &mut __E) -> {cp}::Result<()> {{\n\
618         {body}\n\
619         \x20   }}\n\
620         }}"
621    );
622    ts(&out)
623}
624
625/// Emit the `NsonDeserialize` impl.
626pub(crate) fn generate_de_impl(input: &Input) -> TokenStream {
627    let cp = input.cattr.crate_path.clone();
628    let name = input.ident.clone();
629    let has_flatten = type_has_flag(input, |fa| fa.flatten);
630    let has_borrow = type_has_flag(input, |fa| fa.borrow);
631    if has_flatten && type_has_with(input) {
632        return err("nextjson: `flatten` cannot be combined with `with` / `deserialize_with`");
633    }
634    let (ig, tg, wc) = build_generics(input, &cp, true, has_flatten, has_borrow);
635    let body = match &input.data {
636        Data::Struct(f) => de::deserialize_struct(&name, f, input, &cp, has_flatten),
637        Data::Enum(v) => de::deserialize_enum(&name, v, input, &cp, has_flatten),
638    };
639    let out = format!(
640        "#[automatically_derived]\n\
641         impl {ig} {cp}::NsonDeserialize<'de> for {name}{tg}{wc} {{\n\
642         \x20   fn nextdecode_into<__D: {cp}::FormatDecoder<'de>>(\n\
643         \x20       __d: &mut __D,\n\
644         \x20       __out: &mut {cp}::DecodeSlot<Self>,\n\
645         \x20   ) -> {cp}::Result<()> {{\n\
646         {body}\n\
647         \x20   }}\n\
648         }}"
649    );
650    ts(&out)
651}
652
653fn type_has_flag<F: Fn(&FieldAttrs) -> bool>(input: &Input, f: F) -> bool {
654    match &input.data {
655        Data::Struct(fields) => fields.iter().any(|fld| f(&attr::field_attrs(&fld.attrs))),
656        Data::Enum(variants) => variants
657            .iter()
658            .any(|v| v.fields.iter().any(|fld| f(&attr::field_attrs(&fld.attrs)))),
659    }
660}
661
662fn type_has_with(input: &Input) -> bool {
663    type_has_flag(input, |fa| {
664        fa.with.is_some() || fa.deserialize_with.is_some()
665    })
666}
667
668/// Build a `proc_macro::Ident` (kept for API symmetry).
669#[allow(dead_code)]
670pub(crate) fn ident(name: &str) -> Ident {
671    Ident::new(name, proc_macro::Span::call_site())
672}
673
674// ---------------------------------------------------------------------------
675// Entry points
676// ---------------------------------------------------------------------------
677
678#[proc_macro_derive(NsonSerialize, attributes(njson, nextjson))]
679/// Derive NextJson's native serialization contract and compile-time schema.
680///
681/// Configuration is accepted through `#[njson(...)]`. The generated
682/// implementation writes directly through `NsonSerialize::nextencode` and
683/// exposes `NsonSchema::SCHEMA` without depending on another macro framework.
684pub fn derive_serialize(input: TokenStream) -> TokenStream {
685    match parse_input(input) {
686        Ok(ast) => generate_impls(&ast),
687        Err(e) => err(&e),
688    }
689}
690
691#[proc_macro_derive(NsonDeserialize, attributes(njson, nextjson))]
692/// Derive NextJson's native decoding contract.
693///
694/// Configuration is accepted through `#[njson(...)]`. The generated
695/// implementation decodes through checked `DecodeSlot` state and uses normal
696/// Rust drop semantics for partially initialized fields.
697pub fn derive_deserialize(input: TokenStream) -> TokenStream {
698    match parse_input(input) {
699        Ok(ast) => generate_de_impl(&ast),
700        Err(e) => err(&e),
701    }
702}