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    let body = if let Some(from) = &input.cattr.from {
832        // `from = "T"`: deserialize a `T` then convert into `Self`.
833        format!(
834            "let __v: {from} = <{from} as {cp}::NsonDeserialize<'de>>::nextdecode(__d)?;\n\
835             __out.write(::core::convert::Into::into(__v));\n\
836             ::core::result::Result::Ok(())"
837        )
838    } else if let Some(from) = &input.cattr.try_from {
839        // `try_from = "T"`: deserialize a `T` then fallibly convert.
840        format!(
841            "let __v: {from} = <{from} as {cp}::NsonDeserialize<'de>>::nextdecode(__d)?;\n\
842             let __c: Self = ::core::convert::TryInto::try_into(__v).map_err(|__e| {{\n\
843             \x20   {cp}::FormatError::custom({cp}::__private::ToString::to_string(&__e))\n\
844             }})?;\n\
845             __out.write(__c);\n\
846             ::core::result::Result::Ok(())"
847        )
848    } else {
849        match &input.data {
850            Data::Struct(f) => de::deserialize_struct(&name, f, input, &cp, has_flatten),
851            Data::Enum(v) => de::deserialize_enum(&name, v, input, &cp, has_flatten),
852        }
853    };
854    let out = format!(
855        "#[automatically_derived]\n\
856         impl {ig} {cp}::NsonDeserialize<'de> for {target}{tg_part}{wc} {{\n\
857         \x20   fn nextdecode_into<__D: {cp}::FormatDecoder<'de>>(\n\
858         \x20       __d: &mut __D,\n\
859         \x20       __out: &mut {cp}::DecodeSlot<Self>,\n\
860         \x20   ) -> ::core::result::Result<(), __D::Error> {{\n\
861         {body}\n\
862         \x20   }}\n\
863         }}"
864    );
865    ts(&out)
866}
867
868fn type_has_flag<F: Fn(&FieldAttrs) -> bool>(input: &Input, f: F) -> bool {
869    match &input.data {
870        Data::Struct(fields) => fields.iter().any(|fld| f(&attr::field_attrs(&fld.attrs))),
871        Data::Enum(variants) => variants
872            .iter()
873            .any(|v| v.fields.iter().any(|fld| f(&attr::field_attrs(&fld.attrs)))),
874    }
875}
876
877/// Whether the generated deserializer can fall back to `Default::default()`
878/// for a field whose type is a generic parameter.
879///
880/// True when the container has a default, when any field has a bare
881/// `default` attribute, or when any field is `skip_deserializing` without an
882/// explicit `default = "path"` (which would supply its own value). Field-level
883/// `default = "path"` does not need the bound. `PhantomData` fields are
884/// excluded: `PhantomData<T>: Default` holds for every `T` with no bound.
885fn de_uses_type_param_default(input: &Input) -> bool {
886    if input.cattr.has_default() {
887        return true;
888    }
889    let mut found = false;
890    let mut scan = |f: &Field| {
891        if crate::schema::is_phantom_data(&f.ty) {
892            return;
893        }
894        let fa = attr::field_attrs(&f.attrs);
895        let bare_default = fa.default == Some(String::new());
896        let skip_without_path =
897            fa.skip_deserializing && !matches!(&fa.default, Some(d) if !d.is_empty());
898        if bare_default || skip_without_path {
899            found = true;
900        }
901    };
902    match &input.data {
903        Data::Struct(fields) => {
904            for f in fields.iter() {
905                scan(f);
906            }
907        }
908        Data::Enum(variants) => {
909            for v in variants {
910                for f in v.fields.iter() {
911                    scan(f);
912                }
913            }
914        }
915    }
916    found
917}
918
919fn type_has_with(input: &Input) -> bool {
920    type_has_flag(input, |fa| {
921        fa.with.is_some() || fa.deserialize_with.is_some()
922    })
923}
924
925// ---------------------------------------------------------------------------
926// Entry points
927// ---------------------------------------------------------------------------
928
929#[proc_macro_derive(NsonSerialize, attributes(njson, nextjson, serde))]
930/// Derive NextJson's native serialization contract and compile-time schema.
931///
932/// Configuration is accepted through `#[njson(...)]` (and, for migration
933/// convenience, `#[serde(...)]`). The generated implementation writes
934/// directly through `NsonSerialize::nextencode` and exposes
935/// `NsonSchema::SCHEMA` without depending on another macro framework.
936pub fn derive_serialize(input: TokenStream) -> TokenStream {
937    match parse_input(input) {
938        Ok(ast) => generate_impls(&ast),
939        Err(e) => err(&e),
940    }
941}
942
943#[proc_macro_derive(NsonDeserialize, attributes(njson, nextjson, serde))]
944/// Derive NextJson's native decoding contract.
945///
946/// Configuration is accepted through `#[njson(...)]` (and, for migration
947/// convenience, `#[serde(...)]`). The generated implementation decodes
948/// through checked `DecodeSlot` state and uses normal Rust drop semantics for
949/// partially initialized fields.
950pub fn derive_deserialize(input: TokenStream) -> TokenStream {
951    match parse_input(input) {
952        Ok(ast) => generate_de_impl(&ast),
953        Err(e) => err(&e),
954    }
955}
956
957#[cfg(test)]
958mod tests {
959    use super::*;
960
961    // The `proc_macro` API cannot be used in unit tests (it panics outside a
962    // macro expansion), so these tests build the small AST by hand and cover
963    // the pure logic: attribute parsing, validation, and impl-generics
964    // construction. Parser token-level behavior is exercised by the workspace
965    // integration tests, which compile real derives.
966
967    fn meta_flag(name: &str) -> Meta {
968        Meta::Flag(name.to_string())
969    }
970    fn meta_named(name: &str, value: &str) -> Meta {
971        Meta::Named(name.to_string(), value.to_string())
972    }
973    fn named_field(ident: &str, ty: &str, metas: Vec<Meta>) -> Field {
974        Field {
975            ident: Some(ident.to_string()),
976            ty: ty.to_string(),
977            attrs: metas,
978        }
979    }
980    fn unnamed_field(ty: &str, metas: Vec<Meta>) -> Field {
981        Field {
982            ident: None,
983            ty: ty.to_string(),
984            attrs: metas,
985        }
986    }
987    fn cattr(metas: &[Meta]) -> ContainerAttrs {
988        ContainerAttrs::from_metas(metas)
989    }
990    fn struct_named(cattr: ContainerAttrs, fields: Vec<Field>) -> Input {
991        Input {
992            ident: "S".into(),
993            generics: Generics::default(),
994            data: Data::Struct(Fields::Named(fields)),
995            cattr,
996        }
997    }
998    fn generic_input(
999        ident: &str,
1000        params: Vec<GenericParam>,
1001        where_preds: Vec<String>,
1002        data: Data,
1003        cattr: ContainerAttrs,
1004    ) -> Input {
1005        Input {
1006            ident: ident.to_string(),
1007            generics: Generics {
1008                params,
1009                where_preds,
1010            },
1011            data,
1012            cattr,
1013        }
1014    }
1015
1016    // -- validate_input ------------------------------------------------------
1017
1018    #[test]
1019    fn validate_rejects_transparent_enum() {
1020        let input = Input {
1021            ident: "E".into(),
1022            generics: Generics::default(),
1023            data: Data::Enum(vec![Variant {
1024                ident: "A".into(),
1025                fields: Fields::Unit,
1026                attrs: vec![],
1027            }]),
1028            cattr: cattr(&[meta_flag("transparent")]),
1029        };
1030        let msg = validate_input(&input).expect("must reject transparent enum");
1031        assert!(msg.contains("transparent"));
1032    }
1033
1034    #[test]
1035    fn validate_rejects_transparent_multi_field() {
1036        let input = struct_named(
1037            cattr(&[meta_flag("transparent")]),
1038            vec![
1039                named_field("a", "i32", vec![]),
1040                named_field("b", "i32", vec![]),
1041            ],
1042        );
1043        assert!(validate_input(&input).is_some());
1044    }
1045
1046    #[test]
1047    fn validate_rejects_flatten_on_tuple() {
1048        let input = Input {
1049            ident: "T".into(),
1050            generics: Generics::default(),
1051            data: Data::Struct(Fields::Unnamed(vec![unnamed_field(
1052                "std::collections::BTreeMap<String, i32>",
1053                vec![meta_flag("flatten")],
1054            )])),
1055            cattr: cattr(&[]),
1056        };
1057        let msg = validate_input(&input).expect("must reject flatten on tuple field");
1058        assert!(msg.contains("flatten"), "unexpected error: {msg}");
1059    }
1060
1061    #[test]
1062    fn validate_rejects_flatten_with_skip_if() {
1063        let input = struct_named(
1064            cattr(&[]),
1065            vec![named_field(
1066                "m",
1067                "std::collections::BTreeMap<String, i32>",
1068                vec![
1069                    meta_flag("flatten"),
1070                    meta_named("skip_serializing_if", "Option::is_none"),
1071                ],
1072            )],
1073        );
1074        let msg = validate_input(&input).expect("must reject flatten + skip_serializing_if");
1075        assert!(msg.contains("flatten"), "unexpected error: {msg}");
1076    }
1077
1078    #[test]
1079    fn validate_accepts_valid_combinations() {
1080        let ok = struct_named(
1081            cattr(&[]),
1082            vec![
1083                named_field("a", "i32", vec![]),
1084                named_field(
1085                    "m",
1086                    "std::collections::BTreeMap<String, i32>",
1087                    vec![meta_flag("flatten")],
1088                ),
1089            ],
1090        );
1091        assert!(validate_input(&ok).is_none());
1092
1093        let w = Input {
1094            ident: "W".into(),
1095            generics: Generics::default(),
1096            data: Data::Struct(Fields::Unnamed(vec![unnamed_field("i32", vec![])])),
1097            cattr: cattr(&[meta_flag("transparent")]),
1098        };
1099        assert!(validate_input(&w).is_none());
1100    }
1101
1102    // -- build_generics ------------------------------------------------------
1103
1104    #[test]
1105    fn build_generics_keeps_where_clause_with_bound() {
1106        // `bound` must replace only the auto bounds; the struct's own where
1107        // clause must always survive.
1108        let input = generic_input(
1109            "S",
1110            vec![GenericParam {
1111                kind: ParamKind::Type,
1112                full: "T".into(),
1113                name: "T".into(),
1114            }],
1115            vec!["T: core::fmt::Debug".into()],
1116            Data::Struct(Fields::Named(vec![named_field("v", "T", vec![])])),
1117            cattr(&[meta_named("bound", "\"T: Clone\"")]),
1118        );
1119        let (_, _, wc) = build_generics(&input, "::nextjson", false, false, false);
1120        assert!(wc.contains("Clone"), "missing user bound: {wc}");
1121        assert!(wc.contains("Debug"), "missing struct where clause: {wc}");
1122    }
1123
1124    #[test]
1125    fn build_generics_instantiates_conversion_self_type() {
1126        // Conversion bounds must name `Dst<T>`, never bare `Dst`.
1127        let input = generic_input(
1128            "Dst",
1129            vec![GenericParam {
1130                kind: ParamKind::Type,
1131                full: "T".into(),
1132                name: "T".into(),
1133            }],
1134            vec![],
1135            Data::Struct(Fields::Named(vec![named_field("x", "T", vec![])])),
1136            cattr(&[meta_named("from", "\"Src<T>\"")]),
1137        );
1138        let (_, _, wc) = build_generics(&input, "::nextjson", true, false, false);
1139        assert!(
1140            wc.contains("Into<Dst<T>>"),
1141            "missing instantiated self: {wc}"
1142        );
1143        assert!(!wc.contains("Into<Dst>"), "bare self type: {wc}");
1144    }
1145
1146    #[test]
1147    fn build_generics_adds_default_bound_for_generic_default() {
1148        // Container `default` on a generic struct must add `T: Default`.
1149        let input = generic_input(
1150            "S",
1151            vec![GenericParam {
1152                kind: ParamKind::Type,
1153                full: "T".into(),
1154                name: "T".into(),
1155            }],
1156            vec![],
1157            Data::Struct(Fields::Named(vec![named_field("v", "T", vec![])])),
1158            cattr(&[meta_flag("default")]),
1159        );
1160        let (_, _, wc) = build_generics(&input, "::nextjson", true, false, false);
1161        assert!(
1162            wc.contains("T: ::core::default::Default"),
1163            "missing Default bound: {wc}"
1164        );
1165    }
1166
1167    #[test]
1168    fn build_generics_remote_never_appends_generics_twice() {
1169        // The impl target for `remote` carries its own generic arguments.
1170        let input = generic_input(
1171            "Mirror",
1172            vec![GenericParam {
1173                kind: ParamKind::Type,
1174                full: "T".into(),
1175                name: "T".into(),
1176            }],
1177            vec![],
1178            Data::Struct(Fields::Named(vec![named_field("x", "T", vec![])])),
1179            cattr(&[meta_named("remote", "external::Foreign<T>")]),
1180        );
1181        let (_, _, wc) = build_generics(&input, "::nextjson", false, false, false);
1182        assert!(
1183            wc.contains("T: ::nextjson::NsonSerialize"),
1184            "missing auto bound: {wc}"
1185        );
1186    }
1187
1188    // -- default-bound detection ---------------------------------------------
1189
1190    #[test]
1191    fn default_bound_not_needed_for_phantom_data() {
1192        // A `PhantomData` field must not force `T: Default`.
1193        let input = struct_named(
1194            cattr(&[]),
1195            vec![
1196                named_field("_m", "core::marker::PhantomData<T>", vec![]),
1197                named_field("n", "i32", vec![]),
1198            ],
1199        );
1200        assert!(!de_uses_type_param_default(&input));
1201
1202        // With a container default the bound is required again.
1203        let input2 = struct_named(
1204            cattr(&[meta_flag("default")]),
1205            vec![named_field("v", "T", vec![])],
1206        );
1207        assert!(de_uses_type_param_default(&input2));
1208    }
1209
1210    // -- PhantomData detection ----------------------------------------------
1211
1212    #[test]
1213    fn phantom_detection_spellings() {
1214        assert!(crate::schema::is_phantom_data("PhantomData<T>"));
1215        assert!(crate::schema::is_phantom_data(
1216            "core::marker::PhantomData<T>"
1217        ));
1218        assert!(crate::schema::is_phantom_data(
1219            "::core::marker::PhantomData<T>"
1220        ));
1221        assert!(crate::schema::is_phantom_data(
1222            "std::marker::PhantomData<T>"
1223        ));
1224        assert!(!crate::schema::is_phantom_data("Vec<T>"));
1225        assert!(!crate::schema::is_phantom_data("Option<PhantomData<T>>"));
1226        assert!(!crate::schema::is_phantom_data("Phantom"));
1227    }
1228}