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//! ## Forward compatibility contract
9//!
10//! A derive macro only ever receives a single item's tokens, and this parser
11//! interprets a deliberately **stable grammar subset**: the item header
12//! (`struct` / `enum` + name), the generic parameter list, the `where`
13//! clause, and the field / variant structure, plus `#[njson]` /
14//! `#[nextjson]` / `#[serde]` attributes. Everything inside a field or
15//! generic *type position* is carried through verbatim as an opaque token
16//! sequence, so new Rust syntax that appears in type positions (new
17//! literals, `impl Trait` forms, associated-type paths, ...) needs no parser
18//! change — it is round-tripped unchanged.
19//!
20//! The risk of future item-level grammar changes is handled defensively:
21//! `parse_input` requires that **every** input token be consumed. If a future
22//! Rust release extends item syntax in a way this parser does not understand,
23//! the macro fails with a loud `compile_error!` naming the leftover tokens
24//! instead of silently generating impls from a mis-parsed subset.
25
26#![deny(unsafe_code)]
27#![deny(missing_docs)]
28#![doc(html_root_url = "https://docs.rs/nextjson-derive")]
29
30extern crate proc_macro;
31
32use proc_macro::{Delimiter, Spacing, TokenStream, TokenTree};
33use std::str::FromStr;
34
35mod attr;
36mod case;
37mod de;
38mod schema;
39mod ser;
40
41pub(crate) use attr::{ContainerAttrs, FieldAttrs, Meta, VariantAttrs};
42
43/// Parse a string into a TokenStream.
44pub(crate) fn ts(s: &str) -> TokenStream {
45    TokenStream::from_str(s)
46        .unwrap_or_else(|e| panic!("nextjson-derive: invalid generated tokens: {e:?}"))
47}
48
49/// Build a `compile_error!` expansion from a message.
50pub(crate) fn err(msg: &str) -> TokenStream {
51    ts(&format!("::core::compile_error!({:?})", msg))
52}
53
54/// Error string returned by codegen helpers.
55pub(crate) fn err_str(msg: &str) -> String {
56    msg.to_string()
57}
58
59/// Token cursor over a slice of TokenTrees.
60pub(crate) struct P<'a> {
61    pub toks: &'a [TokenTree],
62    pub i: usize,
63}
64
65impl<'a> P<'a> {
66    pub fn peek(&self) -> Option<&TokenTree> {
67        self.toks.get(self.i)
68    }
69    pub fn next(&mut self) -> Option<TokenTree> {
70        let t = self.toks.get(self.i).cloned();
71        if t.is_some() {
72            self.i += 1;
73        }
74        t
75    }
76    pub fn is_ident(&self, s: &str) -> bool {
77        matches!(self.peek(), Some(TokenTree::Ident(id)) if id.to_string() == s)
78    }
79    pub fn is_punct(&self, ch: char) -> bool {
80        matches!(self.peek(), Some(TokenTree::Punct(p)) if p.as_char() == ch)
81    }
82    pub fn eat_ident(&mut self, s: &str) -> bool {
83        if self.is_ident(s) {
84            self.i += 1;
85            true
86        } else {
87            false
88        }
89    }
90    pub fn eat_punct(&mut self, ch: char) -> bool {
91        if self.is_punct(ch) {
92            self.i += 1;
93            true
94        } else {
95            false
96        }
97    }
98    pub fn expect_ident(&mut self) -> Option<String> {
99        match self.next() {
100            Some(TokenTree::Ident(id)) => Some(id.to_string()),
101            _ => None,
102        }
103    }
104}
105
106/// Join tokens into a re-parseable string, preserving `Joint` spacing so
107/// that punctuation sequences (`::`, `'a`, `->`, `>>`) stay adjacent.
108pub(crate) fn join(toks: &[TokenTree]) -> String {
109    let mut s = String::new();
110    let mut no_space = false;
111    for t in toks {
112        if !s.is_empty() && !no_space {
113            s.push(' ');
114        }
115        no_space = false;
116        match t {
117            TokenTree::Punct(p) => {
118                s.push_str(&p.to_string());
119                no_space = p.spacing() == Spacing::Joint;
120            }
121            _ => s.push_str(&t.to_string()),
122        }
123    }
124    s
125}
126
127/// Split tokens at a top-level separator.
128///
129/// Angle brackets are tracked so that generic types such as
130/// `BTreeMap<String, i32>` stay on a single side of the split.
131pub(crate) fn split_top(toks: &[TokenTree], sep: char) -> Vec<Vec<TokenTree>> {
132    let mut out: Vec<Vec<TokenTree>> = Vec::new();
133    let mut cur: Vec<TokenTree> = Vec::new();
134    let mut angle: usize = 0;
135    for tt in toks {
136        match tt {
137            TokenTree::Group(_) => cur.push(tt.clone()),
138            TokenTree::Punct(p) if p.as_char() == '<' => {
139                angle += 1;
140                cur.push(tt.clone());
141            }
142            TokenTree::Punct(p) if p.as_char() == '>' => {
143                angle = angle.saturating_sub(1);
144                cur.push(tt.clone());
145            }
146            TokenTree::Punct(p) if angle == 0 && p.as_char() == sep => {
147                out.push(std::mem::take(&mut cur));
148            }
149            _ => cur.push(tt.clone()),
150        }
151    }
152    if !cur.is_empty() {
153        out.push(cur);
154    }
155    if out.is_empty() {
156        out.push(Vec::new());
157    }
158    out
159}
160
161/// Read a `<...>` group. proc_macro does not group angle brackets, so this
162/// scans for the matching `>` while ignoring `->` arrow tokens.
163pub(crate) fn read_angle(p: &mut P) -> Option<Vec<TokenTree>> {
164    if !p.eat_punct('<') {
165        return None;
166    }
167    let mut depth = 1usize;
168    let mut out = Vec::new();
169    while let Some(tt) = p.next() {
170        match &tt {
171            TokenTree::Punct(c) if c.as_char() == '<' => {
172                depth += 1;
173                out.push(tt);
174            }
175            TokenTree::Punct(c)
176                if c.as_char() == '-'
177                    && matches!(p.peek(), Some(TokenTree::Punct(n)) if n.as_char() == '>') =>
178            {
179                out.push(tt);
180                out.push(p.next().unwrap());
181            }
182            TokenTree::Punct(c) if c.as_char() == '>' => {
183                if depth == 1 {
184                    return Some(out);
185                }
186                depth -= 1;
187                out.push(tt);
188            }
189            _ => out.push(tt),
190        }
191    }
192    None
193}
194
195// ---------------------------------------------------------------------------
196// AST
197// ---------------------------------------------------------------------------
198
199#[derive(Clone, Copy, PartialEq, Eq)]
200pub(crate) enum ParamKind {
201    Lifetime,
202    Type,
203    Const,
204}
205
206#[derive(Clone)]
207pub(crate) struct GenericParam {
208    pub kind: ParamKind,
209    pub full: String,
210    pub name: String,
211}
212
213#[derive(Clone, Default)]
214pub(crate) struct Generics {
215    pub params: Vec<GenericParam>,
216    pub where_preds: Vec<String>,
217}
218
219#[derive(Clone)]
220pub(crate) struct Field {
221    pub ident: Option<String>,
222    pub ty: String,
223    pub attrs: Vec<attr::Meta>,
224}
225
226#[derive(Clone)]
227pub(crate) enum Fields {
228    Unit,
229    Named(Vec<Field>),
230    Unnamed(Vec<Field>),
231}
232
233impl Fields {
234    pub fn iter(&self) -> core::slice::Iter<'_, Field> {
235        match self {
236            Fields::Unit => [].iter(),
237            Fields::Named(fields) | Fields::Unnamed(fields) => fields.iter(),
238        }
239    }
240}
241
242#[derive(Clone)]
243pub(crate) struct Variant {
244    pub ident: String,
245    pub fields: Fields,
246    pub attrs: Vec<attr::Meta>,
247}
248
249#[derive(Clone)]
250pub(crate) enum Data {
251    Struct(Fields),
252    Enum(Vec<Variant>),
253}
254
255#[derive(Clone)]
256pub(crate) struct Input {
257    pub ident: String,
258    pub generics: Generics,
259    pub data: Data,
260    pub cattr: ContainerAttrs,
261}
262
263// ---------------------------------------------------------------------------
264// Attribute collection
265// ---------------------------------------------------------------------------
266
267/// Collect leading `#[...]` attribute groups.
268fn parse_attrs(p: &mut P) -> Vec<Vec<TokenTree>> {
269    let mut out = Vec::new();
270    while p.is_punct('#') {
271        p.next();
272        if let Some(TokenTree::Group(g)) = p.next() {
273            if g.delimiter() == Delimiter::Bracket {
274                out.push(g.stream().into_iter().collect());
275            }
276        }
277    }
278    out
279}
280
281/// Extract `njson` / `nextjson` metas from a set of attribute groups.
282fn collect_metas(groups: &[Vec<TokenTree>]) -> Vec<attr::Meta> {
283    let mut out = Vec::new();
284    for g in groups {
285        out.extend(attr::metas_from_attr(g));
286    }
287    out
288}
289
290// ---------------------------------------------------------------------------
291// Top-level parse
292// ---------------------------------------------------------------------------
293
294pub(crate) fn parse_input(input: TokenStream) -> Result<Input, String> {
295    let toks: Vec<TokenTree> = input.into_iter().collect();
296    let mut p = P { toks: &toks, i: 0 };
297
298    let attrs = parse_attrs(&mut p);
299    let cattr = ContainerAttrs::from_metas(&collect_metas(&attrs));
300    eat_visibility(&mut p);
301
302    let is_enum = if p.eat_ident("struct") {
303        false
304    } else if p.eat_ident("enum") {
305        true
306    } else {
307        return Err("nextjson: expected `struct` or `enum`".into());
308    };
309
310    let ident = p
311        .expect_ident()
312        .ok_or_else(|| "nextjson: expected type name".to_string())?;
313
314    let mut generics = Generics::default();
315    if let Some(inner) = read_angle(&mut p) {
316        generics = parse_generics(&inner);
317    }
318
319    let data = if !is_enum
320        && matches!(p.peek(), Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Parenthesis)
321    {
322        let Some(TokenTree::Group(body)) = p.next() else {
323            return Err("nextjson: expected a tuple struct body".into());
324        };
325        if p.eat_ident("where") {
326            parse_where_clause(&mut p, &mut generics, false);
327        }
328        // Tuple structs terminate with `;`; consume it so the trailing-input
329        // check below sees a fully parsed item.
330        p.eat_punct(';');
331        let inner: Vec<TokenTree> = body.stream().into_iter().collect();
332        Data::Struct(Fields::Unnamed(parse_unnamed_fields(&inner)))
333    } else {
334        if p.eat_ident("where") {
335            parse_where_clause(&mut p, &mut generics, true);
336        }
337        if !is_enum {
338            match p.next() {
339                Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace => {
340                    let inner: Vec<TokenTree> = g.stream().into_iter().collect();
341                    Data::Struct(Fields::Named(parse_named_fields(&inner)))
342                }
343                Some(TokenTree::Punct(pc)) if pc.as_char() == ';' => Data::Struct(Fields::Unit),
344                _ => return Err("nextjson: expected a struct body".into()),
345            }
346        } else {
347            match p.next() {
348                Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace => {
349                    let inner: Vec<TokenTree> = g.stream().into_iter().collect();
350                    Data::Enum(parse_variants(&inner))
351                }
352                _ => return Err("nextjson: expected an enum body".into()),
353            }
354        }
355    };
356
357    // The derive input must be a single item. Any tokens left over mean the
358    // hand-written parser did not understand part of the declaration; refuse
359    // to generate code from a silently mis-parsed subset (this is the
360    // forward-compatibility guard: if a future Rust release extends item
361    // syntax, the macro fails loudly instead of emitting wrong impls).
362    if p.i != toks.len() {
363        return Err(format!(
364            "nextjson: cannot parse trailing tokens: {}",
365            join(&toks[p.i..])
366        ));
367    }
368
369    Ok(Input {
370        ident,
371        generics,
372        data,
373        cattr,
374    })
375}
376
377fn parse_where_clause(p: &mut P<'_>, generics: &mut Generics, has_braced_body: bool) {
378    let mut tokens = Vec::new();
379    while let Some(token) = p.peek() {
380        let is_body = has_braced_body
381            && p.i + 1 == p.toks.len()
382            && matches!(token, TokenTree::Group(g) if g.delimiter() == Delimiter::Brace);
383        if is_body || matches!(token, TokenTree::Punct(punct) if punct.as_char() == ';') {
384            break;
385        }
386        if let Some(token) = p.next() {
387            tokens.push(token);
388        }
389    }
390    for piece in split_top(&tokens, ',') {
391        let predicate = join(&piece).trim().to_string();
392        if !predicate.is_empty() {
393            generics.where_preds.push(predicate);
394        }
395    }
396}
397
398fn parse_generics(inner: &[TokenTree]) -> Generics {
399    let mut g = Generics::default();
400    for item in split_top(inner, ',') {
401        if item.is_empty() {
402            continue;
403        }
404        let declaration = strip_generic_default(&item);
405        let mut p = P {
406            toks: &declaration,
407            i: 0,
408        };
409        if p.is_punct('\'') {
410            p.next();
411            let name = p.expect_ident().unwrap_or_default();
412            g.params.push(GenericParam {
413                kind: ParamKind::Lifetime,
414                full: join(&declaration),
415                name: format!("'{name}"),
416            });
417        } else if p.eat_ident("const") {
418            let name = p.expect_ident().unwrap_or_default();
419            g.params.push(GenericParam {
420                kind: ParamKind::Const,
421                full: join(&declaration),
422                name,
423            });
424        } else {
425            let name = p.expect_ident().unwrap_or_default();
426            g.params.push(GenericParam {
427                kind: ParamKind::Type,
428                full: join(&declaration),
429                name,
430            });
431        }
432    }
433    g
434}
435
436fn strip_generic_default(tokens: &[TokenTree]) -> Vec<TokenTree> {
437    let mut angle_depth = 0usize;
438    for (index, token) in tokens.iter().enumerate() {
439        match token {
440            TokenTree::Punct(punct) if punct.as_char() == '<' => angle_depth += 1,
441            TokenTree::Punct(punct) if punct.as_char() == '>' => {
442                angle_depth = angle_depth.saturating_sub(1);
443            }
444            TokenTree::Punct(punct) if punct.as_char() == '=' && angle_depth == 0 => {
445                return tokens[..index].to_vec();
446            }
447            _ => {}
448        }
449    }
450    tokens.to_vec()
451}
452
453fn parse_named_fields(inner: &[TokenTree]) -> Vec<Field> {
454    split_top(inner, ',')
455        .iter()
456        .filter(|s| !s.is_empty())
457        .map(|piece| parse_named_field(piece))
458        .collect()
459}
460
461/// Consume an optional `pub` visibility specifier (`pub`, `pub(crate)`,
462/// `pub(super)`, `pub(in path)`). In the proc-macro token stream the
463/// parenthesized part arrives as a `Group` with `Parenthesis` delimiter, not
464/// as a `Punct('(')`, so it must be matched as a group.
465pub(crate) fn eat_visibility(p: &mut P<'_>) {
466    if !p.eat_ident("pub") {
467        return;
468    }
469    if matches!(p.peek(), Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Parenthesis) {
470        p.next();
471    }
472}
473
474fn parse_named_field(piece: &[TokenTree]) -> Field {
475    let mut p = P { toks: piece, i: 0 };
476    let attrs = parse_attrs(&mut p);
477    eat_visibility(&mut p);
478    // Find the field separator ':' at top level, excluding '::'.
479    let mut colon = None;
480    let mut j = p.i;
481    while j < piece.len() {
482        match &piece[j] {
483            TokenTree::Punct(c) if c.as_char() == ':' => {
484                if matches!(piece.get(j + 1), Some(TokenTree::Punct(n)) if n.as_char() == ':') {
485                    j += 2;
486                    continue;
487                }
488                colon = Some(j);
489                break;
490            }
491            _ => j += 1,
492        }
493    }
494    let (ident, ty) = match colon {
495        Some(c) => (
496            Some(join(&piece[p.i..c]).trim().to_string()),
497            join(&piece[c + 1..]).trim().to_string(),
498        ),
499        None => (None, join(&piece[p.i..]).trim().to_string()),
500    };
501    let mut field_metas = collect_metas(&attrs);
502    // `PhantomData` fields are not part of the data model (serde semantics):
503    // skip them on serialize and default them on deserialize. Normalizing at
504    // parse time keeps every codegen path (ser / de / schema) consistent.
505    if crate::schema::is_phantom_data(&ty) {
506        field_metas.push(Meta::Flag("skip".to_string()));
507    }
508    Field {
509        ident,
510        ty,
511        attrs: field_metas,
512    }
513}
514
515fn parse_unnamed_fields(inner: &[TokenTree]) -> Vec<Field> {
516    split_top(inner, ',')
517        .iter()
518        .filter(|s| !s.is_empty())
519        .map(|piece| {
520            let mut p = P { toks: piece, i: 0 };
521            let attrs = parse_attrs(&mut p);
522            eat_visibility(&mut p);
523            Field {
524                ident: None,
525                ty: join(&piece[p.i..]).trim().to_string(),
526                attrs: collect_metas(&attrs),
527            }
528        })
529        .collect()
530}
531
532fn parse_variants(inner: &[TokenTree]) -> Vec<Variant> {
533    split_top(inner, ',')
534        .iter()
535        .filter(|s| !s.is_empty())
536        .map(|piece| {
537            let mut p = P { toks: piece, i: 0 };
538            let attrs = parse_attrs(&mut p);
539            let ident = p.expect_ident().unwrap_or_default();
540            let fields = match p.next() {
541                Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace => {
542                    let inner2: Vec<TokenTree> = g.stream().into_iter().collect();
543                    Fields::Named(parse_named_fields(&inner2))
544                }
545                Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Parenthesis => {
546                    let inner2: Vec<TokenTree> = g.stream().into_iter().collect();
547                    Fields::Unnamed(parse_unnamed_fields(&inner2))
548                }
549                _ => Fields::Unit,
550            };
551            Variant {
552                ident,
553                fields,
554                attrs: collect_metas(&attrs),
555            }
556        })
557        .collect()
558}
559
560// ---------------------------------------------------------------------------
561// Generic helpers for code generation
562// ---------------------------------------------------------------------------
563
564/// Build `(impl_generics, ty_generics, where_clause)` for the impl header.
565pub(crate) fn build_generics(
566    input: &Input,
567    cp: &str,
568    de: bool,
569    has_flatten: bool,
570    has_borrow: bool,
571) -> (String, String, String) {
572    let g = &input.generics;
573    let c = &input.cattr;
574    let name = input.ident.clone();
575    // `remote` implements the traits for an external type; conversion bounds
576    // that mention `Self` must refer to that type instead of the mirror. The
577    // remote path already carries its generic arguments, while a local type
578    // must be written with the mirror's type parameters applied.
579    let (self_ty, remote_typed) = match &c.remote {
580        Some(r) => (r.clone(), true),
581        None => (name.clone(), false),
582    };
583
584    let mut impl_params: Vec<String> = g.params.iter().map(|p| p.full.clone()).collect();
585    if de {
586        impl_params.insert(0, "'de".to_string());
587    }
588    let impl_generics = if impl_params.is_empty() {
589        String::new()
590    } else {
591        format!("<{}>", impl_params.join(", "))
592    };
593
594    let names: Vec<String> = g.params.iter().map(|p| p.name.clone()).collect();
595    let ty_generics = if names.is_empty() {
596        String::new()
597    } else {
598        format!("<{}>", names.join(", "))
599    };
600    // The fully-instantiated `Self` for conversion bounds: for local types the
601    // type parameters must be applied (`Dst<T>`), for remote types the path
602    // already names them (`external::Foreign<T>`).
603    let self_ty_inst = if remote_typed {
604        self_ty.clone()
605    } else {
606        format!("{self_ty}{ty_generics}")
607    };
608
609    // The type's own where-clause predicates are ALWAYS required to name the
610    // type, so they are kept unconditionally. The `bound` attribute only
611    // replaces the *auto-generated per-type-parameter* bounds; serde behaves
612    // the same way.
613    let mut preds: Vec<String> = g.where_preds.clone();
614
615    let directional = if de {
616        c.bound_de.as_ref()
617    } else {
618        c.bound_ser.as_ref()
619    };
620    let bound = directional.or(c.bound.as_ref());
621    let auto_bound = |p: &GenericParam| -> Option<String> {
622        if p.kind != ParamKind::Type {
623            return None;
624        }
625        if de && has_flatten {
626            Some(format!(
627                "{0}: for<'__n> {1}::NsonDeserialize<'__n>",
628                p.name, cp
629            ))
630        } else if de {
631            Some(format!("{}: {}::NsonDeserialize<'de>", p.name, cp))
632        } else {
633            Some(format!("{}: {}::NsonSerialize", p.name, cp))
634        }
635    };
636    if let Some(bound) = bound {
637        let cleaned = bound.trim().trim_matches('"');
638        if !cleaned.is_empty() {
639            for s in cleaned.split(',') {
640                let s = s.trim();
641                if !s.is_empty() {
642                    preds.push(s.to_string());
643                }
644            }
645        }
646    } else {
647        for p in g.params.iter() {
648            if let Some(b) = auto_bound(p) {
649                preds.push(b);
650            }
651        }
652    }
653
654    // Missing-field fallbacks that call `Default::default()` must be able to
655    // name the type parameters they fall back on, so every type parameter
656    // receives a `Default` bound when any fallback can fire for a generic
657    // field. This mirrors serde, which adds `T: Default` for exactly the same
658    // attribute combinations.
659    if de && de_uses_type_param_default(input) {
660        for p in g.params.iter() {
661            if p.kind == ParamKind::Type {
662                preds.push(format!("{}: ::core::default::Default", p.name));
663            }
664        }
665    }
666
667    // Conversion attributes add their own bounds.
668    if de {
669        if let Some(from) = &c.from {
670            preds.push(format!(
671                "{from}: {cp}::NsonDeserialize<'de> + ::core::convert::Into<{self_ty_inst}>"
672            ));
673        }
674        if let Some(from) = &c.try_from {
675            preds.push(format!(
676                "{from}: {cp}::NsonDeserialize<'de> + ::core::convert::TryInto<{self_ty_inst}>"
677            ));
678            preds.push(format!(
679                "<{from} as ::core::convert::TryInto<{self_ty_inst}>>::Error: ::core::fmt::Display"
680            ));
681        }
682    } else {
683        if let Some(into) = &c.into {
684            preds.push(format!("{self_ty_inst}: ::core::clone::Clone"));
685            preds.push(format!("{self_ty_inst}: ::core::convert::Into<{into}>"));
686            preds.push(format!("{into}: {cp}::NsonSerialize"));
687            preds.push(format!("{into}: {cp}::NsonSchema"));
688        }
689    }
690
691    if de && has_borrow {
692        for p in g.params.iter() {
693            if p.kind == ParamKind::Lifetime {
694                preds.push(format!("'de: {}", p.name));
695            }
696        }
697    }
698
699    let where_clause = if preds.is_empty() {
700        String::new()
701    } else {
702        format!(" where {}", preds.join(", "))
703    };
704
705    (impl_generics, ty_generics, where_clause)
706}
707
708/// Validate attribute combinations that serde rejects at compile time.
709///
710/// Returns an error message when the combination is invalid. Called by both
711/// derive entry points so the rejection is identical regardless of which
712/// macro is expanded first.
713fn validate_input(input: &Input) -> Option<String> {
714    // `transparent` is only meaningful on single-field structs.
715    if input.cattr.transparent {
716        match &input.data {
717            Data::Enum(_) => {
718                return Some("nextjson: `transparent` is not supported on enums".to_string());
719            }
720            Data::Struct(Fields::Named(f)) if f.len() != 1 => {
721                return Some("nextjson: `transparent` requires exactly one field".to_string());
722            }
723            Data::Struct(Fields::Unnamed(f)) if f.len() != 1 => {
724                return Some("nextjson: `transparent` requires exactly one field".to_string());
725            }
726            _ => {}
727        }
728    }
729    // `flatten` splices a nested map into the parent object, which is
730    // impossible for positional (unnamed) shapes.
731    if type_has_flag_on(input, |f, fa| fa.flatten && f.ident.is_none()) {
732        return Some(
733            "nextjson: `flatten` is not allowed on tuple structs or tuple variants".to_string(),
734        );
735    }
736    // `flatten` + `skip_serializing_if` conflicts: the decision to skip would
737    // depend on the flattened value, which serde rejects up front.
738    if type_has_flag_on(input, |_f, fa| {
739        fa.flatten && fa.skip_serializing_if.is_some()
740    }) {
741        return Some(
742            "nextjson: `flatten` cannot be combined with `skip_serializing_if`".to_string(),
743        );
744    }
745    None
746}
747
748/// Like [`type_has_flag`] but the predicate also sees the field (needed to
749/// tell named from unnamed fields).
750fn type_has_flag_on<F: Fn(&Field, &FieldAttrs) -> bool>(input: &Input, f: F) -> bool {
751    let check = |fld: &Field| f(fld, &attr::field_attrs(&fld.attrs));
752    match &input.data {
753        Data::Struct(fields) => fields.iter().any(check),
754        Data::Enum(variants) => variants.iter().any(|v| v.fields.iter().any(check)),
755    }
756}
757
758/// Emit the `NsonSchema` + `NsonSerialize` impls.
759pub(crate) fn generate_impls(input: &Input) -> TokenStream {
760    if let Some(msg) = validate_input(input) {
761        return err(&msg);
762    }
763    let cp = input.cattr.crate_path.clone();
764    let name = input.ident.clone();
765    // `remote` implements the traits for the external type itself. The remote
766    // path already names its generic arguments, so the mirror's type-generics
767    // are not appended a second time (`Foreign<T><T>` would not parse).
768    let (target, use_tg) = match &input.cattr.remote {
769        Some(r) => (r.clone(), false),
770        None => (name.clone(), true),
771    };
772    let (ig, tg, wc) = build_generics(input, &cp, false, false, false);
773    let tg_part = if use_tg { tg.as_str() } else { "" };
774    let body = if let Some(into) = &input.cattr.into {
775        // `into = "T"`: serialize by converting `self` to `T` first.
776        format!(
777            "let __v: {into} = ::core::convert::Into::into(self.clone());\n\
778             <{into} as {cp}::NsonSerialize>::nextencode(&__v, __e)"
779        )
780    } else {
781        match &input.data {
782            Data::Struct(f) => ser::serialize_struct(&name, f, input, &cp),
783            Data::Enum(v) => ser::serialize_enum(&name, v, input, &cp),
784        }
785    };
786    let out = format!(
787        "#[automatically_derived]\n\
788         impl {ig} {cp}::NsonSchema for {target}{tg_part}{wc} {{\n\
789         \x20   const SCHEMA: {cp}::TypeSchema = {schema_expr};\n\
790         }}\n\
791         #[automatically_derived]\n\
792         impl {ig} {cp}::NsonSerialize for {target}{tg_part}{wc} {{\n\
793         \x20   fn nextencode<__E: {cp}::FormatEncoder>(&self, __e: &mut __E) -> ::core::result::Result<(), __E::Error> {{\n\
794         {body}\n\
795         \x20   }}\n\
796         }}",
797        schema_expr = schema::schema_expr(input, &cp)
798    );
799    ts(&out)
800}
801
802/// Emit the `NsonDeserialize` impl.
803pub(crate) fn generate_de_impl(input: &Input) -> TokenStream {
804    if let Some(msg) = validate_input(input) {
805        return err(&msg);
806    }
807    let cp = input.cattr.crate_path.clone();
808    let name = input.ident.clone();
809    // Container-level `default` supplies missing-field values from a `Self`
810    // instance, which only makes sense for structs (serde rejects it on
811    // enums as well).
812    if matches!(&input.data, Data::Enum(_)) && input.cattr.has_default() {
813        return err("nextjson: `default` is not supported on enums");
814    }
815    let has_flatten = type_has_flag(input, |fa| fa.flatten);
816    let has_borrow = type_has_flag(input, |fa| fa.borrow);
817    if has_flatten && type_has_with(input) {
818        return err("nextjson: `flatten` cannot be combined with `with` / `deserialize_with`");
819    }
820    if has_flatten && input.cattr.deny_unknown_fields {
821        // flatten consumes every remaining key, so unknown-field rejection is
822        // silently impossible; serde rejects this combination at compile time.
823        return err("nextjson: `deny_unknown_fields` cannot be combined with `flatten`");
824    }
825    let (target, use_tg) = match &input.cattr.remote {
826        Some(r) => (r.clone(), false),
827        None => (name.clone(), true),
828    };
829    let (ig, tg, wc) = build_generics(input, &cp, true, has_flatten, has_borrow);
830    let tg_part = if use_tg { tg.as_str() } else { "" };
831    // `expecting = "..."` overrides the default `type_name`-based description
832    // used in type-mismatch and length-mismatch error messages.
833    let expecting = match &input.cattr.expecting {
834        Some(e) => format!("\n     fn expecting() -> &'static str {{ {:?} }}\n", e),
835        None => String::new(),
836    };
837    let body = if let Some(from) = &input.cattr.from {
838        // `from = "T"`: deserialize a `T` then convert into `Self`.
839        format!(
840            "let __v: {from} = <{from} as {cp}::NsonDeserialize<'de>>::nextdecode(__d)?;\n\
841             __out.write(::core::convert::Into::into(__v));\n\
842             ::core::result::Result::Ok(())"
843        )
844    } else if let Some(from) = &input.cattr.try_from {
845        // `try_from = "T"`: deserialize a `T` then fallibly convert.
846        format!(
847            "let __v: {from} = <{from} as {cp}::NsonDeserialize<'de>>::nextdecode(__d)?;\n\
848             let __c: Self = ::core::convert::TryInto::try_into(__v).map_err(|__e| {{\n\
849             \x20   {cp}::FormatError::custom({cp}::__private::ToString::to_string(&__e))\n\
850             }})?;\n\
851             __out.write(__c);\n\
852             ::core::result::Result::Ok(())"
853        )
854    } else {
855        match &input.data {
856            Data::Struct(f) => de::deserialize_struct(&name, f, input, &cp, has_flatten),
857            Data::Enum(v) => de::deserialize_enum(&name, v, input, &cp, has_flatten),
858        }
859    };
860    let out = format!(
861        "#[automatically_derived]\n\
862         impl {ig} {cp}::NsonDeserialize<'de> for {target}{tg_part}{wc} {{\n\
863         {expecting}\
864         \x20   fn nextdecode_into<__D: {cp}::FormatDecoder<'de>>(\n\
865         \x20       __d: &mut __D,\n\
866         \x20       __out: &mut {cp}::DecodeSlot<Self>,\n\
867         \x20   ) -> ::core::result::Result<(), __D::Error> {{\n\
868         \x20       __d.set_expecting(Self::expecting());\n\
869         {body}\n\
870         \x20   }}\n\
871         }}"
872    );
873    ts(&out)
874}
875
876fn type_has_flag<F: Fn(&FieldAttrs) -> bool>(input: &Input, f: F) -> bool {
877    match &input.data {
878        Data::Struct(fields) => fields.iter().any(|fld| f(&attr::field_attrs(&fld.attrs))),
879        Data::Enum(variants) => variants
880            .iter()
881            .any(|v| v.fields.iter().any(|fld| f(&attr::field_attrs(&fld.attrs)))),
882    }
883}
884
885/// Whether the generated deserializer can fall back to `Default::default()`
886/// for a field whose type is a generic parameter.
887///
888/// True when the container has a default, when any field has a bare
889/// `default` attribute, or when any field is `skip_deserializing` without an
890/// explicit `default = "path"` (which would supply its own value). Field-level
891/// `default = "path"` does not need the bound. `PhantomData` fields are
892/// excluded: `PhantomData<T>: Default` holds for every `T` with no bound.
893fn de_uses_type_param_default(input: &Input) -> bool {
894    if input.cattr.has_default() {
895        return true;
896    }
897    let mut found = false;
898    let mut scan = |f: &Field| {
899        if crate::schema::is_phantom_data(&f.ty) {
900            return;
901        }
902        let fa = attr::field_attrs(&f.attrs);
903        let bare_default = fa.default == Some(String::new());
904        let skip_without_path =
905            fa.skip_deserializing && !matches!(&fa.default, Some(d) if !d.is_empty());
906        if bare_default || skip_without_path {
907            found = true;
908        }
909    };
910    match &input.data {
911        Data::Struct(fields) => {
912            for f in fields.iter() {
913                scan(f);
914            }
915        }
916        Data::Enum(variants) => {
917            for v in variants {
918                for f in v.fields.iter() {
919                    scan(f);
920                }
921            }
922        }
923    }
924    found
925}
926
927fn type_has_with(input: &Input) -> bool {
928    type_has_flag(input, |fa| {
929        fa.with.is_some() || fa.deserialize_with.is_some()
930    })
931}
932
933// ---------------------------------------------------------------------------
934// Entry points
935// ---------------------------------------------------------------------------
936
937#[proc_macro_derive(NsonSerialize, attributes(njson, nextjson, serde))]
938/// Derive NextJson's native serialization contract and compile-time schema.
939///
940/// Configuration is accepted through `#[njson(...)]` (and, for migration
941/// convenience, `#[serde(...)]`). The generated implementation writes
942/// directly through `NsonSerialize::nextencode` and exposes
943/// `NsonSchema::SCHEMA` without depending on another macro framework.
944pub fn derive_serialize(input: TokenStream) -> TokenStream {
945    match parse_input(input) {
946        Ok(ast) => generate_impls(&ast),
947        Err(e) => err(&e),
948    }
949}
950
951#[proc_macro_derive(NsonDeserialize, attributes(njson, nextjson, serde))]
952/// Derive NextJson's native decoding contract.
953///
954/// Configuration is accepted through `#[njson(...)]` (and, for migration
955/// convenience, `#[serde(...)]`). The generated implementation decodes
956/// through checked `DecodeSlot` state and uses normal Rust drop semantics for
957/// partially initialized fields.
958pub fn derive_deserialize(input: TokenStream) -> TokenStream {
959    match parse_input(input) {
960        Ok(ast) => generate_de_impl(&ast),
961        Err(e) => err(&e),
962    }
963}
964
965#[cfg(test)]
966mod tests {
967    use super::*;
968
969    // The `proc_macro` API cannot be used in unit tests (it panics outside a
970    // macro expansion), so these tests build the small AST by hand and cover
971    // the pure logic: attribute parsing, validation, and impl-generics
972    // construction. Parser token-level behavior is exercised by the workspace
973    // integration tests, which compile real derives.
974
975    fn meta_flag(name: &str) -> Meta {
976        Meta::Flag(name.to_string())
977    }
978    fn meta_named(name: &str, value: &str) -> Meta {
979        Meta::Named(name.to_string(), value.to_string())
980    }
981    fn named_field(ident: &str, ty: &str, metas: Vec<Meta>) -> Field {
982        Field {
983            ident: Some(ident.to_string()),
984            ty: ty.to_string(),
985            attrs: metas,
986        }
987    }
988    fn unnamed_field(ty: &str, metas: Vec<Meta>) -> Field {
989        Field {
990            ident: None,
991            ty: ty.to_string(),
992            attrs: metas,
993        }
994    }
995    fn cattr(metas: &[Meta]) -> ContainerAttrs {
996        ContainerAttrs::from_metas(metas)
997    }
998    fn struct_named(cattr: ContainerAttrs, fields: Vec<Field>) -> Input {
999        Input {
1000            ident: "S".into(),
1001            generics: Generics::default(),
1002            data: Data::Struct(Fields::Named(fields)),
1003            cattr,
1004        }
1005    }
1006    fn generic_input(
1007        ident: &str,
1008        params: Vec<GenericParam>,
1009        where_preds: Vec<String>,
1010        data: Data,
1011        cattr: ContainerAttrs,
1012    ) -> Input {
1013        Input {
1014            ident: ident.to_string(),
1015            generics: Generics {
1016                params,
1017                where_preds,
1018            },
1019            data,
1020            cattr,
1021        }
1022    }
1023
1024    // -- validate_input ------------------------------------------------------
1025
1026    #[test]
1027    fn validate_rejects_transparent_enum() {
1028        let input = Input {
1029            ident: "E".into(),
1030            generics: Generics::default(),
1031            data: Data::Enum(vec![Variant {
1032                ident: "A".into(),
1033                fields: Fields::Unit,
1034                attrs: vec![],
1035            }]),
1036            cattr: cattr(&[meta_flag("transparent")]),
1037        };
1038        let msg = validate_input(&input).expect("must reject transparent enum");
1039        assert!(msg.contains("transparent"));
1040    }
1041
1042    #[test]
1043    fn validate_rejects_transparent_multi_field() {
1044        let input = struct_named(
1045            cattr(&[meta_flag("transparent")]),
1046            vec![
1047                named_field("a", "i32", vec![]),
1048                named_field("b", "i32", vec![]),
1049            ],
1050        );
1051        assert!(validate_input(&input).is_some());
1052    }
1053
1054    #[test]
1055    fn validate_rejects_flatten_on_tuple() {
1056        let input = Input {
1057            ident: "T".into(),
1058            generics: Generics::default(),
1059            data: Data::Struct(Fields::Unnamed(vec![unnamed_field(
1060                "std::collections::BTreeMap<String, i32>",
1061                vec![meta_flag("flatten")],
1062            )])),
1063            cattr: cattr(&[]),
1064        };
1065        let msg = validate_input(&input).expect("must reject flatten on tuple field");
1066        assert!(msg.contains("flatten"), "unexpected error: {msg}");
1067    }
1068
1069    #[test]
1070    fn validate_rejects_flatten_with_skip_if() {
1071        let input = struct_named(
1072            cattr(&[]),
1073            vec![named_field(
1074                "m",
1075                "std::collections::BTreeMap<String, i32>",
1076                vec![
1077                    meta_flag("flatten"),
1078                    meta_named("skip_serializing_if", "Option::is_none"),
1079                ],
1080            )],
1081        );
1082        let msg = validate_input(&input).expect("must reject flatten + skip_serializing_if");
1083        assert!(msg.contains("flatten"), "unexpected error: {msg}");
1084    }
1085
1086    #[test]
1087    fn validate_accepts_valid_combinations() {
1088        let ok = struct_named(
1089            cattr(&[]),
1090            vec![
1091                named_field("a", "i32", vec![]),
1092                named_field(
1093                    "m",
1094                    "std::collections::BTreeMap<String, i32>",
1095                    vec![meta_flag("flatten")],
1096                ),
1097            ],
1098        );
1099        assert!(validate_input(&ok).is_none());
1100
1101        let w = Input {
1102            ident: "W".into(),
1103            generics: Generics::default(),
1104            data: Data::Struct(Fields::Unnamed(vec![unnamed_field("i32", vec![])])),
1105            cattr: cattr(&[meta_flag("transparent")]),
1106        };
1107        assert!(validate_input(&w).is_none());
1108    }
1109
1110    // -- build_generics ------------------------------------------------------
1111
1112    #[test]
1113    fn build_generics_keeps_where_clause_with_bound() {
1114        // `bound` must replace only the auto bounds; the struct's own where
1115        // clause must always survive.
1116        let input = generic_input(
1117            "S",
1118            vec![GenericParam {
1119                kind: ParamKind::Type,
1120                full: "T".into(),
1121                name: "T".into(),
1122            }],
1123            vec!["T: core::fmt::Debug".into()],
1124            Data::Struct(Fields::Named(vec![named_field("v", "T", vec![])])),
1125            cattr(&[meta_named("bound", "\"T: Clone\"")]),
1126        );
1127        let (_, _, wc) = build_generics(&input, "::nextjson", false, false, false);
1128        assert!(wc.contains("Clone"), "missing user bound: {wc}");
1129        assert!(wc.contains("Debug"), "missing struct where clause: {wc}");
1130    }
1131
1132    #[test]
1133    fn build_generics_instantiates_conversion_self_type() {
1134        // Conversion bounds must name `Dst<T>`, never bare `Dst`.
1135        let input = generic_input(
1136            "Dst",
1137            vec![GenericParam {
1138                kind: ParamKind::Type,
1139                full: "T".into(),
1140                name: "T".into(),
1141            }],
1142            vec![],
1143            Data::Struct(Fields::Named(vec![named_field("x", "T", vec![])])),
1144            cattr(&[meta_named("from", "\"Src<T>\"")]),
1145        );
1146        let (_, _, wc) = build_generics(&input, "::nextjson", true, false, false);
1147        assert!(
1148            wc.contains("Into<Dst<T>>"),
1149            "missing instantiated self: {wc}"
1150        );
1151        assert!(!wc.contains("Into<Dst>"), "bare self type: {wc}");
1152    }
1153
1154    #[test]
1155    fn build_generics_adds_default_bound_for_generic_default() {
1156        // Container `default` on a generic struct must add `T: Default`.
1157        let input = generic_input(
1158            "S",
1159            vec![GenericParam {
1160                kind: ParamKind::Type,
1161                full: "T".into(),
1162                name: "T".into(),
1163            }],
1164            vec![],
1165            Data::Struct(Fields::Named(vec![named_field("v", "T", vec![])])),
1166            cattr(&[meta_flag("default")]),
1167        );
1168        let (_, _, wc) = build_generics(&input, "::nextjson", true, false, false);
1169        assert!(
1170            wc.contains("T: ::core::default::Default"),
1171            "missing Default bound: {wc}"
1172        );
1173    }
1174
1175    #[test]
1176    fn build_generics_remote_never_appends_generics_twice() {
1177        // The impl target for `remote` carries its own generic arguments.
1178        let input = generic_input(
1179            "Mirror",
1180            vec![GenericParam {
1181                kind: ParamKind::Type,
1182                full: "T".into(),
1183                name: "T".into(),
1184            }],
1185            vec![],
1186            Data::Struct(Fields::Named(vec![named_field("x", "T", vec![])])),
1187            cattr(&[meta_named("remote", "external::Foreign<T>")]),
1188        );
1189        let (_, _, wc) = build_generics(&input, "::nextjson", false, false, false);
1190        assert!(
1191            wc.contains("T: ::nextjson::NsonSerialize"),
1192            "missing auto bound: {wc}"
1193        );
1194    }
1195
1196    // -- default-bound detection ---------------------------------------------
1197
1198    #[test]
1199    fn default_bound_not_needed_for_phantom_data() {
1200        // A `PhantomData` field must not force `T: Default`.
1201        let input = struct_named(
1202            cattr(&[]),
1203            vec![
1204                named_field("_m", "core::marker::PhantomData<T>", vec![]),
1205                named_field("n", "i32", vec![]),
1206            ],
1207        );
1208        assert!(!de_uses_type_param_default(&input));
1209
1210        // With a container default the bound is required again.
1211        let input2 = struct_named(
1212            cattr(&[meta_flag("default")]),
1213            vec![named_field("v", "T", vec![])],
1214        );
1215        assert!(de_uses_type_param_default(&input2));
1216    }
1217
1218    // -- PhantomData detection ----------------------------------------------
1219
1220    #[test]
1221    fn phantom_detection_spellings() {
1222        assert!(crate::schema::is_phantom_data("PhantomData<T>"));
1223        assert!(crate::schema::is_phantom_data(
1224            "core::marker::PhantomData<T>"
1225        ));
1226        assert!(crate::schema::is_phantom_data(
1227            "::core::marker::PhantomData<T>"
1228        ));
1229        assert!(crate::schema::is_phantom_data(
1230            "std::marker::PhantomData<T>"
1231        ));
1232        assert!(!crate::schema::is_phantom_data("Vec<T>"));
1233        assert!(!crate::schema::is_phantom_data("Option<PhantomData<T>>"));
1234        assert!(!crate::schema::is_phantom_data("Phantom"));
1235    }
1236}