Skip to main content

json_schema_rs/code_gen/
rust_backend.rs

1//! Rust codegen backend: emits serde-compatible Rust structs from JSON Schema.
2
3use super::CodeGenBackend;
4use super::CodeGenError;
5use super::CodeGenResult;
6use super::GenerateRustOutput;
7use super::settings::{CodeGenSettings, DedupeMode, ModelNameSource};
8use crate::json_schema::JsonSchema;
9use crate::json_schema::json_schema::AdditionalProperties;
10use crate::json_schema::ref_resolver;
11use crate::sanitizers::{
12    enum_variant_names_with_collision_resolution, sanitize_field_name, sanitize_struct_name,
13};
14use heck::ToSnakeCase;
15use std::cmp::Ordering;
16use std::collections::{BTreeMap, BTreeSet};
17use std::io::{Cursor, Write};
18
19/// Backend that emits Rust structs (serde-compatible).
20#[derive(Debug, Clone, Default)]
21pub struct RustBackend;
22
23impl CodeGenBackend for RustBackend {
24    fn generate(
25        &self,
26        schemas: &[JsonSchema],
27        settings: &CodeGenSettings,
28    ) -> CodeGenResult<GenerateRustOutput> {
29        match settings.dedupe_mode {
30            DedupeMode::Disabled => {
31                let mut per_schema: Vec<Vec<u8>> = Vec::with_capacity(schemas.len());
32                for (index, schema) in schemas.iter().enumerate() {
33                    let mut out = Cursor::new(Vec::new());
34                    emit_rust(schema, &mut out, settings).map_err(|e| CodeGenError::Batch {
35                        index,
36                        source: Box::new(e),
37                    })?;
38                    per_schema.push({
39                        let result = maybe_prepend_btreemap_use(out.into_inner());
40                        let result = maybe_prepend_hash_set_use(result);
41                        #[cfg(feature = "uuid")]
42                        let result = maybe_prepend_uuid_use(result);
43                        result
44                    });
45                }
46                Ok(GenerateRustOutput {
47                    shared: None,
48                    per_schema,
49                })
50            }
51            DedupeMode::Functional | DedupeMode::Full => {
52                generate_rust_with_dedupe(schemas, settings)
53            }
54        }
55    }
56}
57
58/// One struct to emit: name and the object schema (root or nested).
59struct StructToEmit {
60    name: String,
61    schema: JsonSchema,
62}
63
64/// Map from enum value list to (name, description, examples) for dedupe path. Used to resolve enum type names and carry description and examples from first occurrence.
65type EnumValuesToNameMap =
66    BTreeMap<Vec<String>, (String, Option<String>, Option<Vec<serde_json::Value>>)>;
67
68/// One enum to emit: name, sorted deduplicated string values, optional description, and optional examples (from first property schema that contributed this enum).
69struct EnumToEmit {
70    name: String,
71    values: Vec<String>,
72    description: Option<String>,
73    examples: Option<Vec<serde_json::Value>>,
74}
75
76/// One anyOf enum to emit: name and list of (`variant_name`, `rust_type_string`).
77struct AnyOfEnumToEmit {
78    name: String,
79    variants: Vec<(String, String)>,
80}
81
82/// One oneOf enum to emit: name and list of (`variant_name`, `rust_type_string`).
83struct OneOfEnumToEmit {
84    name: String,
85    variants: Vec<(String, String)>,
86}
87
88/// Returns doc comment lines for emission: empty if description is None or whitespace-only; else one line per non-empty trimmed line (no blank lines).
89fn doc_lines(s: Option<&str>) -> Vec<String> {
90    let Some(trimmed) = s.map(str::trim) else {
91        return Vec::new();
92    };
93    if trimmed.is_empty() {
94        return Vec::new();
95    }
96    trimmed
97        .split('\n')
98        .map(str::trim)
99        .filter(|line| !line.is_empty())
100        .map(String::from)
101        .collect()
102}
103
104/// Returns doc comment lines for the # Examples section when examples are present and non-empty.
105/// Each example is serialized to compact JSON. Returns empty vec when None or empty.
106fn examples_doc_lines(examples: Option<&[serde_json::Value]>) -> Vec<String> {
107    let Some(examples) = examples else {
108        return Vec::new();
109    };
110    if examples.is_empty() {
111        return Vec::new();
112    }
113    let mut lines = vec![String::new(), "# Examples".to_string(), String::new()];
114    for ex in examples {
115        if let Ok(s) = serde_json::to_string(ex) {
116            lines.push(s);
117        }
118    }
119    lines
120}
121
122/// Emits `#[deprecated]` or `#[deprecated = "description"]` when schema has `deprecated: true`.
123/// Uses the first line of description for the message when present and non-empty.
124fn emit_deprecated_attr(out: &mut impl Write, schema: &JsonSchema) -> CodeGenResult<()> {
125    if schema.deprecated != Some(true) {
126        return Ok(());
127    }
128    let msg: Option<String> = schema
129        .description
130        .as_deref()
131        .map(str::trim)
132        .filter(|s| !s.is_empty())
133        .and_then(|s| s.split('\n').next())
134        .map(str::trim)
135        .filter(|s| !s.is_empty())
136        .map(|s| s.replace('\\', "\\\\").replace('"', "\\\""));
137    if let Some(m) = msg {
138        writeln!(out, "    #[deprecated = \"{m}\"]")?;
139    } else {
140        writeln!(out, "    #[deprecated]")?;
141    }
142    Ok(())
143}
144
145/// Emits struct-level `#[deprecated]` when schema has `deprecated: true`.
146fn emit_struct_deprecated_attr(out: &mut impl Write, schema: &JsonSchema) -> CodeGenResult<()> {
147    if schema.deprecated != Some(true) {
148        return Ok(());
149    }
150    let msg: Option<String> = schema
151        .description
152        .as_deref()
153        .map(str::trim)
154        .filter(|s| !s.is_empty())
155        .and_then(|s| s.split('\n').next())
156        .map(str::trim)
157        .filter(|s| !s.is_empty())
158        .map(|s| s.replace('\\', "\\\\").replace('"', "\\\""));
159    if let Some(m) = msg {
160        writeln!(out, "#[deprecated = \"{m}\"]")?;
161    } else {
162        writeln!(out, "#[deprecated]")?;
163    }
164    Ok(())
165}
166
167/// Returns sorted, deduplicated string values if the schema is a string enum; otherwise None.
168fn string_enum_values(schema: &JsonSchema) -> Option<Vec<String>> {
169    if !schema.is_string_enum() {
170        return None;
171    }
172    let v: Vec<String> = schema
173        .enum_values
174        .as_ref()
175        .expect("string enum")
176        .iter()
177        .filter_map(|x| x.as_str().map(String::from))
178        .collect();
179    let set: BTreeSet<String> = v.into_iter().collect();
180    let mut out: Vec<String> = set.iter().cloned().collect();
181    out.sort();
182    Some(out)
183}
184
185/// Returns effective string enum values: either from `enum` (string-only) or from `const` when it is a string (single-value).
186fn string_enum_or_const_values(schema: &JsonSchema) -> Option<Vec<String>> {
187    if let Some(values) = string_enum_values(schema) {
188        return Some(values);
189    }
190    if schema.is_string_const() {
191        let s: String = schema
192            .const_value
193            .as_ref()
194            .and_then(|v| v.as_str().map(String::from))
195            .expect("string const");
196        return Some(vec![s]);
197    }
198    None
199}
200
201/// For dedupe key: additionalProperties as Forbid or Schema(sub key).
202#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
203enum AdditionalPropertiesDedupe {
204    Forbid,
205    Schema(Box<DedupeKey>),
206}
207
208/// Key for deduplication: canonical representation of an object schema for a given mode.
209/// Implements `Ord` + `Eq` for use in `BTreeMap`. Full mode includes id, description, comment, and examples; Functional mode excludes them (key ignores $id).
210#[derive(Debug, Clone)]
211struct DedupeKey {
212    id: Option<String>,
213    type_: Option<String>,
214    properties: BTreeMap<String, DedupeKey>,
215    additional_properties: Option<AdditionalPropertiesDedupe>,
216    required: Option<Vec<String>>,
217    title: Option<String>,
218    description: Option<String>,
219    comment: Option<String>,
220    deprecated: Option<bool>,
221    examples: Option<Vec<serde_json::Value>>,
222    items: Option<Box<DedupeKey>>,
223    unique_items: Option<bool>,
224    min_items: Option<u64>,
225    max_items: Option<u64>,
226    min_length: Option<u64>,
227    max_length: Option<u64>,
228    pattern: Option<String>,
229    format: Option<String>,
230    default_value: Option<serde_json::Value>,
231}
232
233impl PartialEq for DedupeKey {
234    fn eq(&self, other: &Self) -> bool {
235        self.id == other.id
236            && self.type_ == other.type_
237            && self.properties == other.properties
238            && self.additional_properties == other.additional_properties
239            && self.required == other.required
240            && self.title == other.title
241            && self.description == other.description
242            && self.comment == other.comment
243            && self.deprecated == other.deprecated
244            && self.examples == other.examples
245            && self.items == other.items
246            && self.unique_items == other.unique_items
247            && self.min_items == other.min_items
248            && self.max_items == other.max_items
249            && self.min_length == other.min_length
250            && self.max_length == other.max_length
251            && self.pattern == other.pattern
252            && self.format == other.format
253            && self.default_value == other.default_value
254    }
255}
256
257impl Eq for DedupeKey {}
258
259impl PartialOrd for DedupeKey {
260    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
261        Some(self.cmp(other))
262    }
263}
264
265impl Ord for DedupeKey {
266    fn cmp(&self, other: &Self) -> Ordering {
267        self.id
268            .cmp(&other.id)
269            .then_with(|| self.type_.cmp(&other.type_))
270            .then_with(|| {
271                self.properties
272                    .keys()
273                    .cmp(other.properties.keys())
274                    .then_with(|| {
275                        for (k, v) in &self.properties {
276                            if let Some(ov) = other.properties.get(k) {
277                                let c = v.cmp(ov);
278                                if c != Ordering::Equal {
279                                    return c;
280                                }
281                            }
282                        }
283                        self.properties.len().cmp(&other.properties.len())
284                    })
285            })
286            .then_with(|| self.additional_properties.cmp(&other.additional_properties))
287            .then_with(|| compare_option_vec(self.required.as_ref(), other.required.as_ref()))
288            .then_with(|| self.title.cmp(&other.title))
289            .then_with(|| self.description.cmp(&other.description))
290            .then_with(|| self.comment.cmp(&other.comment))
291            .then_with(|| self.deprecated.cmp(&other.deprecated))
292            .then_with(|| compare_option_vec_value(self.examples.as_ref(), other.examples.as_ref()))
293            .then_with(|| self.items.cmp(&other.items))
294            .then_with(|| self.unique_items.cmp(&other.unique_items))
295            .then_with(|| self.min_items.cmp(&other.min_items))
296            .then_with(|| self.max_items.cmp(&other.max_items))
297            .then_with(|| self.min_length.cmp(&other.min_length))
298            .then_with(|| self.max_length.cmp(&other.max_length))
299            .then_with(|| self.pattern.cmp(&other.pattern))
300            .then_with(|| self.format.cmp(&other.format))
301            .then_with(|| {
302                compare_option_value(self.default_value.as_ref(), other.default_value.as_ref())
303            })
304    }
305}
306
307fn compare_option_value(a: Option<&serde_json::Value>, b: Option<&serde_json::Value>) -> Ordering {
308    match (a, b) {
309        (None, None) => Ordering::Equal,
310        (None, Some(_)) => Ordering::Less,
311        (Some(_), None) => Ordering::Greater,
312        (Some(a_val), Some(b_val)) => {
313            let a_str: String = serde_json::to_string(a_val).unwrap_or_default();
314            let b_str: String = serde_json::to_string(b_val).unwrap_or_default();
315            a_str.cmp(&b_str)
316        }
317    }
318}
319
320fn compare_option_vec_value(
321    a: Option<&Vec<serde_json::Value>>,
322    b: Option<&Vec<serde_json::Value>>,
323) -> Ordering {
324    match (a, b) {
325        (None, None) => Ordering::Equal,
326        (None, Some(_)) => Ordering::Less,
327        (Some(_), None) => Ordering::Greater,
328        (Some(aa), Some(bb)) => {
329            let len_cmp: Ordering = aa.len().cmp(&bb.len());
330            if len_cmp != Ordering::Equal {
331                return len_cmp;
332            }
333            for (a_val, b_val) in aa.iter().zip(bb.iter()) {
334                let c: Ordering = compare_option_value(Some(a_val), Some(b_val));
335                if c != Ordering::Equal {
336                    return c;
337                }
338            }
339            Ordering::Equal
340        }
341    }
342}
343
344/// Returns true when the JSON default value equals the Rust type default (so we can use `#[serde(default)]`).
345fn json_value_equals_rust_type_default(
346    value: &serde_json::Value,
347    _type_str: &str,
348    is_optional: bool,
349) -> bool {
350    if is_optional {
351        return value.is_null();
352    }
353    match value {
354        serde_json::Value::Null => true,
355        serde_json::Value::Bool(b) => !*b,
356        serde_json::Value::Number(n) => {
357            if n.as_i64() == Some(0) {
358                return true;
359            }
360            if n.as_f64() == Some(0.0) {
361                return true;
362            }
363            false
364        }
365        serde_json::Value::String(s) => s.is_empty(),
366        serde_json::Value::Array(a) => a.is_empty(),
367        serde_json::Value::Object(o) => o.is_empty(),
368    }
369}
370
371/// Returns the default function name for a struct field (e.g. `default_root_my_field`).
372/// Uses `snake_case` for both struct and field names per Rust naming conventions.
373fn default_function_name(struct_name: &str, field_name: &str) -> String {
374    let struct_snake: String = struct_name.to_snake_case();
375    let field_snake: String = field_name.to_snake_case();
376    format!("default_{struct_snake}_{field_snake}")
377}
378
379/// Returns Rust expression for a JSON default value for the given type (e.g. `Some("foo".to_string())` for Option<String>).
380fn json_default_to_rust_expr(
381    value: &serde_json::Value,
382    _ty: &str,
383    is_optional: bool,
384) -> Option<String> {
385    let inner: String = match value {
386        serde_json::Value::Bool(b) => b.to_string(),
387        serde_json::Value::Number(n) => {
388            if n.is_i64() {
389                n.as_i64().unwrap().to_string()
390            } else {
391                n.as_f64().unwrap().to_string()
392            }
393        }
394        serde_json::Value::String(s) => {
395            let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
396            format!("\"{escaped}\".to_string()")
397        }
398        serde_json::Value::Null | serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
399            return None;
400        }
401    };
402    let expr: String = if is_optional {
403        format!("Some({inner})")
404    } else {
405        inner
406    };
407    Some(expr)
408}
409
410fn compare_option_vec(a: Option<&Vec<String>>, b: Option<&Vec<String>>) -> Ordering {
411    match (a, b) {
412        (None, None) => Ordering::Equal,
413        (None, Some(_)) => Ordering::Less,
414        (Some(_), None) => Ordering::Greater,
415        (Some(aa), Some(bb)) => aa.cmp(bb),
416    }
417}
418
419/// If the buffer contains "`HashSet`", insert `use std::collections::HashSet;` after the serde use line.
420fn maybe_prepend_btreemap_use(mut buf: Vec<u8>) -> Vec<u8> {
421    if !buf.windows(8).any(|w| w == b"BTreeMap") {
422        return buf;
423    }
424    let needle = b"use serde::{Deserialize, Serialize};\n";
425    let pos = buf
426        .windows(needle.len())
427        .position(|w| w == needle)
428        .map(|i| i + needle.len());
429    if let Some(insert_at) = pos {
430        let line = b"use std::collections::BTreeMap;\n";
431        buf.splice(insert_at..insert_at, line.iter().copied());
432    }
433    buf
434}
435
436fn maybe_prepend_hash_set_use(mut buf: Vec<u8>) -> Vec<u8> {
437    if !buf.windows(7).any(|w| w == b"HashSet") {
438        return buf;
439    }
440    let needle = b"use serde::{Deserialize, Serialize};\n";
441    let pos = buf
442        .windows(needle.len())
443        .position(|w| w == needle)
444        .map(|i| i + needle.len());
445    if let Some(insert_at) = pos {
446        let hash_set_use = b"use std::collections::HashSet;\n";
447        buf.splice(insert_at..insert_at, hash_set_use.iter().copied());
448    }
449    buf
450}
451
452/// If the buffer contains "`Uuid`", insert `use uuid::Uuid;` after the HashSet use line (if present) or after the serde use line.
453#[cfg(feature = "uuid")]
454fn maybe_prepend_uuid_use(mut buf: Vec<u8>) -> Vec<u8> {
455    if !buf.windows(4).any(|w| w == b"Uuid") {
456        return buf;
457    }
458    let hash_set_needle = b"use std::collections::HashSet;\n";
459    let serde_needle = b"use serde::{Deserialize, Serialize};\n";
460    let insert_at = buf
461        .windows(hash_set_needle.len())
462        .position(|w| w == hash_set_needle)
463        .map(|i| i + hash_set_needle.len())
464        .or_else(|| {
465            buf.windows(serde_needle.len())
466                .position(|w| w == serde_needle)
467                .map(|i| i + serde_needle.len())
468        });
469    if let Some(pos) = insert_at {
470        let uuid_use = b"use uuid::Uuid;\n";
471        buf.splice(pos..pos, uuid_use.iter().copied());
472    }
473    buf
474}
475
476/// Emits `#[derive(..., ToJsonSchema)]` and optional `#[json_schema(title = "...")]` for a struct.
477/// Uses `json_schema_rs_macro::ToJsonSchema` so generated code compiles when the macro crate is a dependency.
478/// Emits struct doc comment from schema.description when non-empty.
479fn emit_struct_derive_and_attrs(
480    out: &mut impl Write,
481    name: &str,
482    schema: &JsonSchema,
483) -> CodeGenResult<()> {
484    for line in doc_lines(schema.description.as_deref()) {
485        writeln!(out, "/// {line}")?;
486    }
487    for line in examples_doc_lines(schema.examples.as_deref()) {
488        writeln!(out, "/// {line}")?;
489    }
490    emit_struct_deprecated_attr(out, schema)?;
491    writeln!(
492        out,
493        "#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]"
494    )?;
495    if let Some(ref t) = schema.title {
496        let t = t.trim();
497        if !t.is_empty() {
498            let escaped = t.replace('\\', "\\\\").replace('"', "\\\"");
499            writeln!(out, "#[json_schema(title = \"{escaped}\")]")?;
500        }
501    }
502    if let Some(ref i) = schema.id {
503        let escaped = i.replace('\\', "\\\\").replace('"', "\\\"");
504        writeln!(out, "#[json_schema(id = \"{escaped}\")]")?;
505    }
506    if schema
507        .additional_properties
508        .as_ref()
509        .is_some_and(|ap| matches!(ap, AdditionalProperties::Forbid))
510    {
511        writeln!(out, "#[serde(deny_unknown_fields)]")?;
512    }
513    writeln!(out, "pub struct {name} {{")?;
514    Ok(())
515}
516
517impl DedupeKey {
518    fn from_schema(schema: &JsonSchema, mode: DedupeMode) -> Self {
519        let properties: BTreeMap<String, DedupeKey> = schema
520            .properties
521            .iter()
522            .map(|(k, v)| (k.clone(), DedupeKey::from_schema(v, mode)))
523            .collect();
524        let items: Option<Box<DedupeKey>> = schema
525            .items
526            .as_ref()
527            .filter(|_| schema.type_.as_deref() == Some("array"))
528            .map(|s| Box::new(DedupeKey::from_schema(s, mode)));
529        let unique_items: Option<bool> = if schema.type_.as_deref() == Some("array") {
530            schema.unique_items
531        } else {
532            None
533        };
534        let min_items: Option<u64> = if schema.type_.as_deref() == Some("array") {
535            schema.min_items
536        } else {
537            None
538        };
539        let max_items: Option<u64> = if schema.type_.as_deref() == Some("array") {
540            schema.max_items
541        } else {
542            None
543        };
544        let min_length: Option<u64> = if schema.type_.as_deref() == Some("string") {
545            schema.min_length
546        } else {
547            None
548        };
549        let max_length: Option<u64> = if schema.type_.as_deref() == Some("string") {
550            schema.max_length
551        } else {
552            None
553        };
554        let pattern: Option<String> = if schema.type_.as_deref() == Some("string") {
555            schema.pattern.clone()
556        } else {
557            None
558        };
559        let format: Option<String> = if schema.type_.as_deref() == Some("string") {
560            schema.format.clone()
561        } else {
562            None
563        };
564        let additional_properties: Option<AdditionalPropertiesDedupe> =
565            match schema.additional_properties.as_ref() {
566                None | Some(AdditionalProperties::Allow) => None,
567                Some(AdditionalProperties::Forbid) => Some(AdditionalPropertiesDedupe::Forbid),
568                Some(AdditionalProperties::Schema(s)) => Some(AdditionalPropertiesDedupe::Schema(
569                    Box::new(DedupeKey::from_schema(s, mode)),
570                )),
571            };
572        DedupeKey {
573            id: match mode {
574                DedupeMode::Full => schema.id.clone(),
575                DedupeMode::Functional | DedupeMode::Disabled => None,
576            },
577            type_: schema.type_.clone(),
578            properties,
579            additional_properties,
580            required: schema.required.clone(),
581            title: schema.title.clone(),
582            description: match mode {
583                DedupeMode::Full => schema.description.clone(),
584                DedupeMode::Functional | DedupeMode::Disabled => None,
585            },
586            comment: match mode {
587                DedupeMode::Full => schema.comment.clone(),
588                DedupeMode::Functional | DedupeMode::Disabled => None,
589            },
590            deprecated: match mode {
591                DedupeMode::Full => schema.deprecated,
592                DedupeMode::Functional | DedupeMode::Disabled => None,
593            },
594            examples: match mode {
595                DedupeMode::Full => schema.examples.clone(),
596                DedupeMode::Functional | DedupeMode::Disabled => None,
597            },
598            items,
599            unique_items,
600            min_items,
601            max_items,
602            min_length,
603            max_length,
604            pattern,
605            format,
606            default_value: schema.default_value.clone(),
607        }
608    }
609}
610
611/// Compute struct/type name from title, property key, and root fallback per settings.
612fn struct_name_from(
613    title: Option<&str>,
614    from_key: Option<&str>,
615    is_root: bool,
616    settings: &CodeGenSettings,
617) -> String {
618    let title_trimmed: Option<&str> = title.filter(|t| !t.trim().is_empty()).map(str::trim);
619    let from_key_s: Option<&str> = from_key;
620
621    let (first, second) = match settings.model_name_source {
622        ModelNameSource::TitleFirst => (title_trimmed, from_key_s),
623        ModelNameSource::PropertyKeyFirst => (from_key_s, title_trimmed),
624    };
625
626    first
627        .map(sanitize_struct_name)
628        .or_else(|| second.map(sanitize_struct_name))
629        .unwrap_or_else(|| {
630            if is_root {
631                "Root".to_string()
632            } else {
633                "Unnamed".to_string()
634            }
635        })
636}
637
638const I64_MAX_AS_F64: f64 = 9_223_372_036_854_775_807.0_f64; // i64::MAX, exactly representable
639
640/// Returns the Rust type string for an integer or number schema using `minimum` and `maximum` when both present and valid; otherwise fallback to `i64` or `f64`.
641fn rust_numeric_type_for_schema(schema: &JsonSchema) -> String {
642    if schema.is_integer() {
643        let min: Option<f64> = schema.minimum;
644        let max: Option<f64> = schema.maximum;
645        #[expect(clippy::cast_precision_loss)]
646        let i64_min_f64: f64 = i64::MIN as f64;
647        let (min_i64, max_i64): (Option<i64>, Option<i64>) = match (min, max) {
648            (Some(mi), Some(ma)) if mi <= ma => {
649                let valid_min: bool =
650                    mi.fract() == 0.0 && (i64_min_f64..=I64_MAX_AS_F64).contains(&mi);
651                let valid_max: bool =
652                    ma.fract() == 0.0 && (i64_min_f64..=I64_MAX_AS_F64).contains(&ma);
653                if valid_min && valid_max {
654                    #[expect(clippy::cast_possible_truncation)]
655                    let min_i: i64 = mi as i64;
656                    #[expect(clippy::cast_possible_truncation)]
657                    let max_i: i64 = ma as i64;
658                    (Some(min_i), Some(max_i))
659                } else {
660                    (None, None)
661                }
662            }
663            _ => (None, None),
664        };
665        if let (Some(lo), Some(hi)) = (min_i64, max_i64) {
666            if lo >= 0 {
667                if hi <= i64::from(u8::MAX) {
668                    return "u8".to_string();
669                }
670                if hi <= i64::from(u16::MAX) {
671                    return "u16".to_string();
672                }
673                if hi <= i64::from(u32::MAX) {
674                    return "u32".to_string();
675                }
676                return "u64".to_string();
677            }
678            if lo >= i64::from(i8::MIN) && hi <= i64::from(i8::MAX) {
679                return "i8".to_string();
680            }
681            if lo >= i64::from(i16::MIN) && hi <= i64::from(i16::MAX) {
682                return "i16".to_string();
683            }
684            if lo >= i64::from(i32::MIN) && hi <= i64::from(i32::MAX) {
685                return "i32".to_string();
686            }
687        }
688        return "i64".to_string();
689    }
690    if schema.is_number() {
691        let min: Option<f64> = schema.minimum;
692        let max: Option<f64> = schema.maximum;
693        if let (Some(mi), Some(ma)) = (min, max)
694            && mi <= ma
695            && mi >= f64::from(f32::MIN)
696            && ma <= f64::from(f32::MAX)
697            && mi.is_finite()
698            && ma.is_finite()
699        {
700            return "f32".to_string();
701        }
702        return "f64".to_string();
703    }
704    unreachable!("rust_numeric_type_for_schema only called for integer or number schema");
705}
706
707/// Returns true when the schema represents a type that is Hash + Eq (string, integer, number, or enum).
708/// Used to decide whether to emit `HashSet<T>` for array with uniqueItems: true.
709fn item_schema_is_hashable(schema: &JsonSchema) -> bool {
710    !schema.is_object_with_properties() && !schema.is_array_with_items()
711}
712
713/// True if schema is object-like for allOf merge: type "object" or non-empty properties.
714fn is_object_like_for_merge(schema: &JsonSchema) -> bool {
715    schema.type_.as_deref() == Some("object") || !schema.properties.is_empty()
716}
717
718/// Merge an array of object-like schemas (allOf) into a single schema. Errors on empty array,
719/// non-object-like subschema, or conflicting property types/bounds/enums.
720pub(crate) fn merge_all_of(schemas: &[JsonSchema]) -> CodeGenResult<JsonSchema> {
721    if schemas.is_empty() {
722        return Err(CodeGenError::AllOfMergeEmpty);
723    }
724    for (index, s) in schemas.iter().enumerate() {
725        if !is_object_like_for_merge(s) {
726            return Err(CodeGenError::AllOfMergeNonObjectSubschema { index });
727        }
728    }
729    let mut merged = JsonSchema::default();
730    for s in schemas {
731        merge_object_schema_into(&mut merged, s, "")?;
732    }
733    merged.type_ = Some("object".to_string());
734    Ok(merged)
735}
736
737/// Merge one object schema into `target`. Used for allOf merge. `parent_key` is for error messages.
738fn merge_object_schema_into(
739    target: &mut JsonSchema,
740    other: &JsonSchema,
741    parent_key: &str,
742) -> CodeGenResult<()> {
743    for (k, other_prop) in &other.properties {
744        let key_for_errors = if parent_key.is_empty() {
745            k.clone()
746        } else {
747            format!("{parent_key}.{k}")
748        };
749        if let Some(target_prop) = target.properties.get_mut(k) {
750            let merged_prop = merge_property_schemas(target_prop, other_prop, &key_for_errors)?;
751            *target_prop = merged_prop;
752        } else {
753            target.properties.insert(k.clone(), other_prop.clone());
754        }
755    }
756    // Required: union, dedupe, first-occurrence order
757    let mut required: Vec<String> = target.required.clone().unwrap_or_default();
758    for r in other.required.as_deref().unwrap_or(&[]) {
759        if !required.contains(r) {
760            required.push(r.clone());
761        }
762    }
763    target.required = if required.is_empty() {
764        None
765    } else {
766        Some(required)
767    };
768    if target.title.as_deref().map_or("", str::trim).is_empty() {
769        target.title.clone_from(&other.title);
770    }
771    if target
772        .description
773        .as_deref()
774        .map_or("", str::trim)
775        .is_empty()
776    {
777        target.description.clone_from(&other.description);
778    }
779    if target.comment.is_none() {
780        target.comment.clone_from(&other.comment);
781    }
782    if target.deprecated.is_none() {
783        target.deprecated = other.deprecated;
784    }
785    if target.examples.is_none() {
786        target.examples.clone_from(&other.examples);
787    }
788    Ok(())
789}
790
791/// Merge two property subschemas (same key in different allOf branches). Returns merged schema or error on conflict.
792fn merge_property_schemas(
793    a: &JsonSchema,
794    b: &JsonSchema,
795    property_key: &str,
796) -> CodeGenResult<JsonSchema> {
797    if a.is_object_with_properties() && b.is_object_with_properties() {
798        let mut merged = a.clone();
799        merge_object_schema_into(&mut merged, b, property_key)?;
800        return Ok(merged);
801    }
802    if a.is_array_with_items() && b.is_array_with_items() {
803        let a_items = a.items.as_ref().expect("array with items").as_ref();
804        let b_items = b.items.as_ref().expect("array with items").as_ref();
805        let merged_items = merge_property_schemas(a_items, b_items, &format!("{property_key}[]"))?;
806        let mut out = a.clone();
807        out.items = Some(Box::new(merged_items));
808        return Ok(out);
809    }
810    let type_a = a.type_.as_deref();
811    let type_b = b.type_.as_deref();
812    if type_a != type_b {
813        return Err(CodeGenError::AllOfMergeConflictingPropertyType {
814            property_key: property_key.to_string(),
815            subschema_indices: vec![], // we don't have indices in this context; message still clear
816        });
817    }
818    if a.is_string() && b.is_string() {
819        let mut out = a.clone();
820        if out.min_length.is_none() {
821            out.min_length = b.min_length;
822        }
823        if out.max_length.is_none() {
824            out.max_length = b.max_length;
825        }
826        if let (Some(pa), Some(pb)) = (&a.pattern, &b.pattern) {
827            if pa != pb {
828                return Err(CodeGenError::AllOfMergeConflictingPattern {
829                    property_key: property_key.to_string(),
830                });
831            }
832        } else if out.pattern.is_none() {
833            out.pattern.clone_from(&b.pattern);
834        }
835        if out.format.is_none() {
836            out.format.clone_from(&b.format);
837        }
838        if let (Some(ea), Some(eb)) = (&a.enum_values, &b.enum_values) {
839            if ea != eb {
840                return Err(CodeGenError::AllOfMergeConflictingEnum {
841                    property_key: property_key.to_string(),
842                });
843            }
844        } else if b.enum_values.is_some() {
845            out.enum_values.clone_from(&b.enum_values);
846        }
847        if let (Some(ca), Some(cb)) = (&a.const_value, &b.const_value) {
848            if ca != cb {
849                return Err(CodeGenError::AllOfMergeConflictingConst {
850                    property_key: property_key.to_string(),
851                });
852            }
853        } else if b.const_value.is_some() {
854            out.const_value.clone_from(&b.const_value);
855        }
856        return Ok(out);
857    }
858    if a.is_integer() && b.is_integer() || a.is_number() && b.is_number() {
859        let mut out = a.clone();
860        merge_numeric_bounds(&mut out, b, property_key, "minimum", "maximum")?;
861        return Ok(out);
862    }
863    if a.is_string_enum() && b.is_string_enum() {
864        if a.enum_values != b.enum_values {
865            return Err(CodeGenError::AllOfMergeConflictingEnum {
866                property_key: property_key.to_string(),
867            });
868        }
869        return Ok(a.clone());
870    }
871    if type_a.is_some() || type_b.is_some() {
872        return Err(CodeGenError::AllOfMergeConflictingPropertyType {
873            property_key: property_key.to_string(),
874            subschema_indices: vec![],
875        });
876    }
877    Ok(a.clone())
878}
879
880fn merge_numeric_bounds(
881    target: &mut JsonSchema,
882    other: &JsonSchema,
883    property_key: &str,
884    min_kw: &str,
885    max_kw: &str,
886) -> CodeGenResult<()> {
887    let (t_min, t_max) = (target.minimum, target.maximum);
888    let (o_min, o_max) = (other.minimum, other.maximum);
889    let new_min = match (t_min, o_min) {
890        (Some(t), Some(o)) => Some(t.max(o)),
891        (a, None) | (None, a) => a,
892    };
893    let new_max = match (t_max, o_max) {
894        (Some(t), Some(o)) => Some(t.min(o)),
895        (a, None) | (None, a) => a,
896    };
897    if let (Some(mi), Some(ma)) = (new_min, new_max)
898        && mi > ma
899    {
900        return Err(CodeGenError::AllOfMergeConflictingNumericBounds {
901            property_key: property_key.to_string(),
902            keyword: format!("{min_kw}/{max_kw}"),
903        });
904    }
905    target.minimum = new_min;
906    target.maximum = new_max;
907    Ok(())
908}
909
910/// Resolve allOf for codegen: if schema has non-empty `all_of`, merge and return; otherwise return clone.
911pub(crate) fn resolve_all_of_for_codegen(schema: &JsonSchema) -> CodeGenResult<JsonSchema> {
912    match &schema.all_of {
913        Some(all) if !all.is_empty() => merge_all_of(all),
914        Some(_) => Err(CodeGenError::AllOfMergeEmpty),
915        None => Ok(schema.clone()),
916    }
917}
918
919/// Returns the Rust type string for a schema (used for array item type and nested types).
920/// Unsupported types yield `serde_json::Value`.
921fn rust_type_for_item_schema(
922    root: &JsonSchema,
923    schema: &JsonSchema,
924    from_key: Option<&str>,
925    enum_values_to_name: Option<&BTreeMap<Vec<String>, String>>,
926    key_to_name: Option<&BTreeMap<DedupeKey, String>>,
927    settings: &CodeGenSettings,
928    mode: DedupeMode,
929) -> CodeGenResult<String> {
930    let mut def_key: Option<String> = None;
931    let schema: &JsonSchema = if let Some(ref_str) = schema.ref_.as_deref() {
932        match ref_resolver::parse_ref(ref_str) {
933            Ok(
934                ref_resolver::ParsedRef::Defs(name) | ref_resolver::ParsedRef::Definitions(name),
935            ) => def_key = Some(name),
936            Ok(ref_resolver::ParsedRef::Root) => {}
937            Err(e) => {
938                return Err(CodeGenError::RefResolution {
939                    ref_str: ref_str.to_string(),
940                    reason: format!("{e:?}"),
941                });
942            }
943        }
944
945        ref_resolver::resolve_schema_ref_transitive(root, schema).map_err(|e| {
946            CodeGenError::RefResolution {
947                ref_str: ref_str.to_string(),
948                reason: format!("{e:?}"),
949            }
950        })?
951    } else {
952        schema
953    };
954    if let Some(values) = string_enum_or_const_values(schema)
955        && let Some(m) = enum_values_to_name
956        && let Some(name) = m.get(&values)
957    {
958        return Ok(name.clone());
959    }
960    if schema.is_string()
961        || (schema.enum_values.as_ref().is_some_and(|v| !v.is_empty()) && !schema.is_string_enum())
962        || (schema.const_value.is_some() && !schema.is_string_const())
963    {
964        #[cfg(feature = "uuid")]
965        {
966            if schema.format.as_deref() == Some("uuid") {
967                return Ok("Uuid".to_string());
968            }
969        }
970        return Ok("String".to_string());
971    }
972    if schema.is_integer() {
973        return Ok(rust_numeric_type_for_schema(schema));
974    }
975    if schema.is_number() {
976        return Ok(rust_numeric_type_for_schema(schema));
977    }
978    if schema.is_boolean() {
979        return Ok("bool".to_string());
980    }
981    if schema.is_object_with_properties() {
982        if let Some(key) = def_key.as_deref() {
983            return Ok(sanitize_struct_name(key));
984        }
985        let name: String = if let Some(m) = key_to_name {
986            let key = DedupeKey::from_schema(schema, mode);
987            m.get(&key).cloned().unwrap_or_else(|| {
988                struct_name_from(schema.title.as_deref(), from_key, false, settings)
989            })
990        } else {
991            struct_name_from(schema.title.as_deref(), from_key, false, settings)
992        };
993        return Ok(name);
994    }
995    if schema.is_array_with_items() {
996        let item_schema: &JsonSchema = schema.items.as_ref().expect("array with items").as_ref();
997        let inner: String = rust_type_for_item_schema(
998            root,
999            item_schema,
1000            from_key,
1001            enum_values_to_name,
1002            key_to_name,
1003            settings,
1004            mode,
1005        )?;
1006        let use_hash_set: bool =
1007            schema.unique_items == Some(true) && item_schema_is_hashable(item_schema);
1008        return Ok(if use_hash_set {
1009            format!("HashSet<{inner}>")
1010        } else {
1011            format!("Vec<{inner}>")
1012        });
1013    }
1014    Ok("serde_json::Value".to_string())
1015}
1016
1017/// Collect all string enums from a schema (and nested properties). Dedupe by value list; first occurrence wins the name, description, and examples.
1018fn collect_enums(
1019    root: &JsonSchema,
1020    schema: &JsonSchema,
1021    settings: &CodeGenSettings,
1022) -> CodeGenResult<Vec<EnumToEmit>> {
1023    let mut key_to_name_desc_examples: EnumValuesToNameMap = BTreeMap::new();
1024    let mut stack: Vec<JsonSchema> = vec![schema.clone()];
1025    while let Some(node) = stack.pop() {
1026        let (node, _) = resolve_ref_for_codegen(root, &node, None)?;
1027        for (key, prop_schema) in &node.properties {
1028            let (prop_effective, from_key) = resolve_ref_for_codegen(root, prop_schema, Some(key))?;
1029            if let Some(values) = string_enum_or_const_values(&prop_effective) {
1030                key_to_name_desc_examples
1031                    .entry(values.clone())
1032                    .or_insert_with(|| {
1033                        let name: String = struct_name_from(
1034                            prop_effective.title.as_deref(),
1035                            from_key.as_deref(),
1036                            false,
1037                            settings,
1038                        );
1039                        let description: Option<String> = prop_effective
1040                            .description
1041                            .as_ref()
1042                            .filter(|s| !s.trim().is_empty())
1043                            .cloned();
1044                        let examples: Option<Vec<serde_json::Value>> =
1045                            prop_effective.examples.clone();
1046                        (name, description, examples)
1047                    });
1048            }
1049            if prop_effective.is_object_with_properties() {
1050                stack.push(prop_effective.clone());
1051            }
1052            if prop_effective.is_array_with_items()
1053                && let Some(ref items) = prop_effective.items
1054            {
1055                let (items_effective, items_from_key) =
1056                    resolve_ref_for_codegen(root, items.as_ref(), Some(key))?;
1057                if let Some(values) = string_enum_or_const_values(&items_effective) {
1058                    key_to_name_desc_examples
1059                        .entry(values.clone())
1060                        .or_insert_with(|| {
1061                            let name: String = struct_name_from(
1062                                items_effective.title.as_deref(),
1063                                items_from_key.as_deref(),
1064                                false,
1065                                settings,
1066                            );
1067                            let description: Option<String> = items_effective
1068                                .description
1069                                .as_ref()
1070                                .filter(|s| !s.trim().is_empty())
1071                                .cloned();
1072                            let examples: Option<Vec<serde_json::Value>> =
1073                                items_effective.examples.clone();
1074                            (name, description, examples)
1075                        });
1076                }
1077                if items_effective.is_object_with_properties() {
1078                    stack.push(items_effective);
1079                }
1080            }
1081        }
1082        if let Some(ref any_of) = node.any_of {
1083            for sub in any_of {
1084                stack.push(sub.clone());
1085            }
1086        }
1087        if let Some(ref one_of) = node.one_of {
1088            for sub in one_of {
1089                stack.push(sub.clone());
1090            }
1091        }
1092    }
1093    Ok(key_to_name_desc_examples
1094        .into_iter()
1095        .map(|(values, (name, description, examples))| EnumToEmit {
1096            name,
1097            values,
1098            description,
1099            examples,
1100        })
1101        .collect())
1102}
1103
1104/// Collect all anyOf enums from a schema (root and nested). Each node with non-empty anyOf produces one enum.
1105fn collect_anyof_enums(
1106    root: &JsonSchema,
1107    schema: &JsonSchema,
1108    settings: &CodeGenSettings,
1109    enum_values_to_name: &BTreeMap<Vec<String>, String>,
1110) -> CodeGenResult<Vec<AnyOfEnumToEmit>> {
1111    let mut out: Vec<AnyOfEnumToEmit> = vec![];
1112    let mut stack: Vec<(JsonSchema, Option<String>)> = vec![(schema.clone(), None)];
1113    while let Some((node, from_key)) = stack.pop() {
1114        let (node, from_key) = resolve_ref_for_codegen(root, &node, from_key.as_deref())?;
1115        if let Some(ref any_of) = node.any_of {
1116            if any_of.is_empty() {
1117                return Err(CodeGenError::AnyOfEmpty);
1118            }
1119            let name = match &from_key {
1120                Some(k) => sanitize_struct_name(k) + "AnyOf",
1121                None => node.title.as_deref().map_or_else(
1122                    || "RootAnyOf".to_string(),
1123                    |t| sanitize_struct_name(t) + "AnyOf",
1124                ),
1125            };
1126            let mut variants = Vec::with_capacity(any_of.len());
1127            for (i, sub) in any_of.iter().enumerate() {
1128                let resolved = resolve_all_of_for_codegen(sub)?;
1129                let variant_from_key =
1130                    format!("{}_Variant{i}", from_key.as_deref().unwrap_or("Root"));
1131                let ty = rust_type_for_item_schema(
1132                    root,
1133                    &resolved,
1134                    Some(&variant_from_key),
1135                    Some(enum_values_to_name),
1136                    None,
1137                    settings,
1138                    DedupeMode::Full,
1139                )?;
1140                variants.push((format!("Variant{i}"), ty));
1141            }
1142            out.push(AnyOfEnumToEmit { name, variants });
1143            for sub in any_of {
1144                let resolved = resolve_all_of_for_codegen(sub)?;
1145                stack.push((resolved, None));
1146            }
1147        }
1148        for (key, prop_schema) in &node.properties {
1149            stack.push((prop_schema.clone(), Some(key.clone())));
1150        }
1151    }
1152    Ok(out)
1153}
1154
1155/// Collect all oneOf enums from a schema (root and nested). Each node with non-empty oneOf produces one enum.
1156fn collect_oneof_enums(
1157    root: &JsonSchema,
1158    schema: &JsonSchema,
1159    settings: &CodeGenSettings,
1160    enum_values_to_name: &BTreeMap<Vec<String>, String>,
1161) -> CodeGenResult<Vec<OneOfEnumToEmit>> {
1162    let mut out: Vec<OneOfEnumToEmit> = vec![];
1163    let mut stack: Vec<(JsonSchema, Option<String>)> = vec![(schema.clone(), None)];
1164    while let Some((node, from_key)) = stack.pop() {
1165        let (node, from_key) = resolve_ref_for_codegen(root, &node, from_key.as_deref())?;
1166        if let Some(ref one_of) = node.one_of {
1167            if one_of.is_empty() {
1168                return Err(CodeGenError::OneOfEmpty);
1169            }
1170            let name = match &from_key {
1171                Some(k) => sanitize_struct_name(k) + "OneOf",
1172                None => node.title.as_deref().map_or_else(
1173                    || "RootOneOf".to_string(),
1174                    |t| sanitize_struct_name(t) + "OneOf",
1175                ),
1176            };
1177            let mut variants = Vec::with_capacity(one_of.len());
1178            for (i, sub) in one_of.iter().enumerate() {
1179                let resolved = resolve_all_of_for_codegen(sub)?;
1180                let variant_from_key =
1181                    format!("{}_Variant{i}", from_key.as_deref().unwrap_or("Root"));
1182                let ty = rust_type_for_item_schema(
1183                    root,
1184                    &resolved,
1185                    Some(&variant_from_key),
1186                    Some(enum_values_to_name),
1187                    None,
1188                    settings,
1189                    DedupeMode::Full,
1190                )?;
1191                variants.push((format!("Variant{i}"), ty));
1192            }
1193            out.push(OneOfEnumToEmit { name, variants });
1194            for sub in one_of {
1195                let resolved = resolve_all_of_for_codegen(sub)?;
1196                stack.push((resolved, None));
1197            }
1198        }
1199        for (key, prop_schema) in &node.properties {
1200            stack.push((prop_schema.clone(), Some(key.clone())));
1201        }
1202    }
1203    Ok(out)
1204}
1205
1206/// Collect all object schemas that need a struct in topological order (children before parents).
1207/// Uses an explicit stack to avoid recursion and stack overflow on deep schemas.
1208/// Resolves allOf for each node before use (merge on-the-fly).
1209fn resolve_ref_for_codegen(
1210    root: &JsonSchema,
1211    schema: &JsonSchema,
1212    fallback_from_key: Option<&str>,
1213) -> CodeGenResult<(JsonSchema, Option<String>)> {
1214    let mut from_key: Option<String> = fallback_from_key.map(String::from);
1215    let Some(ref_str) = schema.ref_.as_deref() else {
1216        return Ok((schema.clone(), from_key));
1217    };
1218
1219    match ref_resolver::parse_ref(ref_str) {
1220        Ok(ref_resolver::ParsedRef::Defs(name) | ref_resolver::ParsedRef::Definitions(name)) => {
1221            from_key = Some(name);
1222        }
1223        Ok(ref_resolver::ParsedRef::Root) => {}
1224        Err(e) => {
1225            return Err(CodeGenError::RefResolution {
1226                ref_str: ref_str.to_string(),
1227                reason: format!("{e:?}"),
1228            });
1229        }
1230    }
1231
1232    let resolved: &JsonSchema =
1233        ref_resolver::resolve_schema_ref_transitive(root, schema).map_err(|e| {
1234            CodeGenError::RefResolution {
1235                ref_str: ref_str.to_string(),
1236                reason: format!("{e:?}"),
1237            }
1238        })?;
1239    Ok((resolved.clone(), from_key))
1240}
1241
1242#[expect(clippy::too_many_lines)]
1243fn collect_structs(
1244    root: &JsonSchema,
1245    schema: &JsonSchema,
1246    from_key: Option<&str>,
1247    out: &mut Vec<StructToEmit>,
1248    seen: &mut BTreeSet<String>,
1249    settings: &CodeGenSettings,
1250) -> CodeGenResult<()> {
1251    let (schema, from_key_opt) = resolve_ref_for_codegen(root, schema, from_key)?;
1252    if !schema.is_object_with_properties() {
1253        return Ok(());
1254    }
1255
1256    // Phase 1: iterative post-order DFS to collect (schema, from_key) so children come before parents.
1257    let mut post_order: Vec<(JsonSchema, Option<String>, bool)> = Vec::new();
1258    let mut stack: Vec<(JsonSchema, Option<String>, usize, bool)> = Vec::new();
1259    stack.push((
1260        schema.clone(),
1261        from_key_opt.clone(),
1262        0,
1263        from_key_opt.is_none(),
1264    ));
1265
1266    while let Some((schema_node, from_key_opt, index, is_root)) = stack.pop() {
1267        let keys: Vec<String> = schema_node.properties.keys().cloned().collect();
1268        if index < keys.len() {
1269            let key: String = keys.get(index).unwrap().clone();
1270            let child: JsonSchema = schema_node.properties.get(&key).unwrap().clone();
1271            let child_resolved = resolve_all_of_for_codegen(&child)?;
1272            stack.push((schema_node, from_key_opt, index + 1, is_root));
1273            if child_resolved
1274                .any_of
1275                .as_ref()
1276                .is_some_and(|v| !v.is_empty())
1277            {
1278                for (i, sub) in child_resolved.any_of.as_ref().unwrap().iter().enumerate() {
1279                    let sub_resolved = resolve_all_of_for_codegen(sub)?;
1280                    let variant_key = format!("{key}_Variant{i}");
1281                    let (sub_effective, sub_from_key) =
1282                        resolve_ref_for_codegen(root, &sub_resolved, Some(&variant_key))?;
1283                    if sub_effective.is_object_with_properties() {
1284                        stack.push((sub_effective, sub_from_key, 0, false));
1285                    } else if sub_resolved.is_array_with_items()
1286                        && let Some(ref items) = sub_resolved.items
1287                    {
1288                        let items_resolved = resolve_all_of_for_codegen(items.as_ref())?;
1289                        let (items_effective, items_from_key) =
1290                            resolve_ref_for_codegen(root, &items_resolved, Some(&variant_key))?;
1291                        if items_effective.is_object_with_properties() {
1292                            stack.push((items_effective, items_from_key, 0, false));
1293                        }
1294                    }
1295                }
1296            } else if child_resolved
1297                .one_of
1298                .as_ref()
1299                .is_some_and(|v| !v.is_empty())
1300            {
1301                for (i, sub) in child_resolved.one_of.as_ref().unwrap().iter().enumerate() {
1302                    let sub_resolved = resolve_all_of_for_codegen(sub)?;
1303                    let variant_key = format!("{key}_Variant{i}");
1304                    let (sub_effective, sub_from_key) =
1305                        resolve_ref_for_codegen(root, &sub_resolved, Some(&variant_key))?;
1306                    if sub_effective.is_object_with_properties() {
1307                        stack.push((sub_effective, sub_from_key, 0, false));
1308                    } else if sub_resolved.is_array_with_items()
1309                        && let Some(ref items) = sub_resolved.items
1310                    {
1311                        let items_resolved = resolve_all_of_for_codegen(items.as_ref())?;
1312                        let (items_effective, items_from_key) =
1313                            resolve_ref_for_codegen(root, &items_resolved, Some(&variant_key))?;
1314                        if items_effective.is_object_with_properties() {
1315                            stack.push((items_effective, items_from_key, 0, false));
1316                        }
1317                    }
1318                }
1319            } else {
1320                let (child_effective, child_from_key) =
1321                    resolve_ref_for_codegen(root, &child_resolved, Some(&key))?;
1322                if child_effective.is_object_with_properties() {
1323                    stack.push((child_effective, child_from_key, 0, false));
1324                } else if child_effective.is_array_with_items()
1325                    && let Some(ref items) = child_effective.items
1326                {
1327                    let items_resolved = resolve_all_of_for_codegen(items.as_ref())?;
1328                    let (items_effective, items_from_key) =
1329                        resolve_ref_for_codegen(root, &items_resolved, Some(&key))?;
1330                    if items_effective.is_object_with_properties() {
1331                        stack.push((items_effective, items_from_key, 0, false));
1332                    }
1333                }
1334            }
1335        } else {
1336            post_order.push((schema_node, from_key_opt, is_root));
1337        }
1338    }
1339
1340    // Phase 2: emit in post-order, dedupe by name (first occurrence wins).
1341    for (schema_node, from_key_opt, is_root) in post_order {
1342        let name: String = struct_name_from(
1343            schema_node.title.as_deref(),
1344            from_key_opt.as_deref(),
1345            is_root,
1346            settings,
1347        );
1348
1349        if seen.contains(&name) {
1350            continue;
1351        }
1352        seen.insert(name.clone());
1353
1354        out.push(StructToEmit {
1355            name,
1356            schema: schema_node,
1357        });
1358    }
1359    Ok(())
1360}
1361
1362/// Collect (`schema_idx`, `candidate_name`, schema) for every struct from all schemas in post-order
1363/// (children before parents) per schema. No name dedupe.
1364#[expect(clippy::too_many_lines)]
1365fn collect_structs_all_schemas(
1366    schemas: &[JsonSchema],
1367    settings: &CodeGenSettings,
1368) -> CodeGenResult<Vec<(usize, String, JsonSchema)>> {
1369    let mut out: Vec<(usize, String, JsonSchema)> = Vec::new();
1370    for (schema_idx, schema_root) in schemas.iter().enumerate() {
1371        let (effective_root, root_from_key) =
1372            resolve_ref_for_codegen(schema_root, schema_root, None)?;
1373        if !effective_root.is_object_with_properties() {
1374            continue;
1375        }
1376
1377        let mut post_order: Vec<(JsonSchema, Option<String>, bool)> = Vec::new();
1378        let mut stack: Vec<(JsonSchema, Option<String>, usize, bool)> = Vec::new();
1379        let is_root: bool = root_from_key.is_none();
1380        stack.push((effective_root.clone(), root_from_key, 0, is_root));
1381
1382        while let Some((schema_node, from_key_opt, index, is_root)) = stack.pop() {
1383            let keys: Vec<String> = schema_node.properties.keys().cloned().collect();
1384            if index < keys.len() {
1385                let key: String = keys[index].clone();
1386                let child: JsonSchema = schema_node.properties.get(&key).unwrap().clone();
1387                let child_resolved = resolve_all_of_for_codegen(&child)?;
1388
1389                stack.push((schema_node, from_key_opt, index + 1, is_root));
1390
1391                if child_resolved
1392                    .any_of
1393                    .as_ref()
1394                    .is_some_and(|v| !v.is_empty())
1395                {
1396                    for (i, sub) in child_resolved.any_of.as_ref().unwrap().iter().enumerate() {
1397                        let sub_resolved = resolve_all_of_for_codegen(sub)?;
1398                        let variant_key = format!("{key}_Variant{i}");
1399                        let (sub_effective, sub_from_key) = resolve_ref_for_codegen(
1400                            schema_root,
1401                            &sub_resolved,
1402                            Some(&variant_key),
1403                        )?;
1404                        if sub_effective.is_object_with_properties() {
1405                            stack.push((sub_effective, sub_from_key, 0, false));
1406                        } else if sub_effective.is_array_with_items()
1407                            && let Some(ref items) = sub_effective.items
1408                        {
1409                            let items_resolved = resolve_all_of_for_codegen(items.as_ref())?;
1410                            let (items_effective, items_from_key) = resolve_ref_for_codegen(
1411                                schema_root,
1412                                &items_resolved,
1413                                Some(&variant_key),
1414                            )?;
1415                            if items_effective.is_object_with_properties() {
1416                                stack.push((items_effective, items_from_key, 0, false));
1417                            }
1418                        }
1419                    }
1420                } else if child_resolved
1421                    .one_of
1422                    .as_ref()
1423                    .is_some_and(|v| !v.is_empty())
1424                {
1425                    for (i, sub) in child_resolved.one_of.as_ref().unwrap().iter().enumerate() {
1426                        let sub_resolved = resolve_all_of_for_codegen(sub)?;
1427                        let variant_key = format!("{key}_Variant{i}");
1428                        let (sub_effective, sub_from_key) = resolve_ref_for_codegen(
1429                            schema_root,
1430                            &sub_resolved,
1431                            Some(&variant_key),
1432                        )?;
1433                        if sub_effective.is_object_with_properties() {
1434                            stack.push((sub_effective, sub_from_key, 0, false));
1435                        } else if sub_effective.is_array_with_items()
1436                            && let Some(ref items) = sub_effective.items
1437                        {
1438                            let items_resolved = resolve_all_of_for_codegen(items.as_ref())?;
1439                            let (items_effective, items_from_key) = resolve_ref_for_codegen(
1440                                schema_root,
1441                                &items_resolved,
1442                                Some(&variant_key),
1443                            )?;
1444                            if items_effective.is_object_with_properties() {
1445                                stack.push((items_effective, items_from_key, 0, false));
1446                            }
1447                        }
1448                    }
1449                } else {
1450                    let (child_effective, child_from_key) =
1451                        resolve_ref_for_codegen(schema_root, &child_resolved, Some(&key))?;
1452                    if child_effective.is_object_with_properties() {
1453                        stack.push((child_effective, child_from_key, 0, false));
1454                    } else if child_effective.is_array_with_items()
1455                        && let Some(ref items) = child_effective.items
1456                    {
1457                        let items_resolved = resolve_all_of_for_codegen(items.as_ref())?;
1458                        let (items_effective, items_from_key) =
1459                            resolve_ref_for_codegen(schema_root, &items_resolved, Some(&key))?;
1460                        if items_effective.is_object_with_properties() {
1461                            stack.push((items_effective, items_from_key, 0, false));
1462                        }
1463                    }
1464                }
1465            } else {
1466                post_order.push((schema_node, from_key_opt, is_root));
1467            }
1468        }
1469
1470        for (schema_node, from_key_opt, is_root) in post_order {
1471            let name: String = struct_name_from(
1472                schema_node.title.as_deref(),
1473                from_key_opt.as_deref(),
1474                is_root,
1475                settings,
1476            );
1477            out.push((schema_idx, name, schema_node));
1478        }
1479    }
1480    Ok(out)
1481}
1482
1483/// Generate Rust with dedupe (Functional or Full mode). Returns shared buffer (if any) and per-schema buffers.
1484#[expect(clippy::too_many_lines)]
1485#[expect(clippy::type_complexity)]
1486fn generate_rust_with_dedupe(
1487    schemas: &[JsonSchema],
1488    settings: &CodeGenSettings,
1489) -> CodeGenResult<GenerateRustOutput> {
1490    let mode: DedupeMode = settings.dedupe_mode;
1491
1492    let resolved_schemas: Vec<JsonSchema> = schemas
1493        .iter()
1494        .enumerate()
1495        .map(|(i, s)| {
1496            resolve_all_of_for_codegen(s).map_err(|e| CodeGenError::Batch {
1497                index: i,
1498                source: Box::new(e),
1499            })
1500        })
1501        .collect::<CodeGenResult<Vec<_>>>()?;
1502
1503    let mut enum_values_to_name: EnumValuesToNameMap = BTreeMap::new();
1504    for schema in &resolved_schemas {
1505        for e in collect_enums(schema, schema, settings)? {
1506            enum_values_to_name
1507                .entry(e.values.clone())
1508                .or_insert_with(|| (e.name.clone(), e.description.clone(), e.examples.clone()));
1509        }
1510    }
1511    let all_enums: Vec<EnumToEmit> = enum_values_to_name
1512        .iter()
1513        .map(|(values, (name, description, examples))| EnumToEmit {
1514            name: name.clone(),
1515            values: values.clone(),
1516            description: description.clone(),
1517            examples: examples.clone(),
1518        })
1519        .collect();
1520
1521    let collected: Vec<(usize, String, JsonSchema)> =
1522        collect_structs_all_schemas(&resolved_schemas, settings)?;
1523
1524    // Build BTreeMap: DedupeKey -> (canonical_name, schema, occurrences)
1525    let mut map: BTreeMap<DedupeKey, (String, JsonSchema, Vec<(usize, String)>)> = BTreeMap::new();
1526    for (schema_idx, name, schema) in &collected {
1527        let key: DedupeKey = DedupeKey::from_schema(schema, mode);
1528        map.entry(key)
1529            .or_insert_with(|| (name.clone(), schema.clone(), Vec::new()))
1530            .2
1531            .push((*schema_idx, name.clone()));
1532    }
1533
1534    let shared_names: BTreeSet<String> = map
1535        .iter()
1536        .filter(|(_, (_, _, occs))| occs.len() > 1)
1537        .map(|(_, (canonical_name, _, _))| canonical_name.clone())
1538        .collect();
1539
1540    let canonical_name_to_first_schema_idx: BTreeMap<String, usize> = {
1541        let mut out: BTreeMap<String, usize> = BTreeMap::new();
1542        for (canonical_name, _, occs) in map.values() {
1543            let first_idx: usize = occs.iter().map(|(i, _)| *i).min().unwrap_or(0);
1544            out.entry(canonical_name.clone())
1545                .and_modify(|v| *v = (*v).min(first_idx))
1546                .or_insert(first_idx);
1547        }
1548        out
1549    };
1550
1551    let key_to_canonical_name: BTreeMap<DedupeKey, String> = map
1552        .iter()
1553        .map(|(k, (canonical, _, _))| (k.clone(), canonical.clone()))
1554        .collect();
1555
1556    let key_to_canonical: BTreeMap<DedupeKey, (String, JsonSchema)> = map
1557        .iter()
1558        .map(|(k, (cn, schema, _))| (k.clone(), (cn.clone(), schema.clone())))
1559        .collect();
1560
1561    if shared_names.is_empty() {
1562        let mut per_schema: Vec<Vec<u8>> = Vec::with_capacity(resolved_schemas.len());
1563        for (index, schema) in resolved_schemas.iter().enumerate() {
1564            let mut out = Cursor::new(Vec::new());
1565            emit_rust(schema, &mut out, settings).map_err(|e| CodeGenError::Batch {
1566                index,
1567                source: Box::new(e),
1568            })?;
1569            per_schema.push({
1570                let result = maybe_prepend_btreemap_use(out.into_inner());
1571                let result = maybe_prepend_hash_set_use(result);
1572                #[cfg(feature = "uuid")]
1573                let result = maybe_prepend_uuid_use(result);
1574                result
1575            });
1576        }
1577        return Ok(GenerateRustOutput {
1578            shared: None,
1579            per_schema,
1580        });
1581    }
1582
1583    // Shared structs: (canonical_name, schema) in dependency order
1584    let shared_structs: Vec<(String, JsonSchema)> = {
1585        let mut v: Vec<(String, JsonSchema)> = key_to_canonical
1586            .iter()
1587            .filter(|(_, (cn, _))| shared_names.contains(cn))
1588            .map(|(_, (cn, s))| (cn.clone(), s.clone()))
1589            .collect();
1590        let order: Vec<String> = v.iter().map(|(n, _)| n.clone()).collect();
1591        let mut deps: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
1592        for (name, schema) in &v {
1593            let mut set: BTreeSet<String> = BTreeSet::new();
1594            for prop_schema in schema.properties.values() {
1595                if prop_schema.is_object_with_properties() {
1596                    let prop_key = DedupeKey::from_schema(prop_schema, mode);
1597                    if let Some(cn) = key_to_canonical_name.get(&prop_key)
1598                        && shared_names.contains(cn)
1599                    {
1600                        set.insert(cn.clone());
1601                    }
1602                }
1603                if prop_schema.is_array_with_items()
1604                    && let Some(ref items) = prop_schema.items
1605                    && items.is_object_with_properties()
1606                {
1607                    let item_key = DedupeKey::from_schema(items, mode);
1608                    if let Some(cn) = key_to_canonical_name.get(&item_key)
1609                        && shared_names.contains(cn)
1610                    {
1611                        set.insert(cn.clone());
1612                    }
1613                }
1614            }
1615            deps.insert(name.clone(), set);
1616        }
1617        topo_sort_by_deps(&order, &deps, &mut v);
1618        v
1619    };
1620
1621    let shared_buffer: Vec<u8> = {
1622        let mut out = Cursor::new(Vec::new());
1623        writeln!(
1624            out,
1625            "//! Generated by json-schema-rs. Do not edit manually."
1626        )?;
1627        writeln!(out)?;
1628        writeln!(out, "use serde::{{Deserialize, Serialize}};")?;
1629        writeln!(out)?;
1630        for e in &all_enums {
1631            let pairs: Vec<(String, String)> =
1632                enum_variant_names_with_collision_resolution(&e.values);
1633            emit_enum_from_pairs(
1634                &mut out,
1635                &e.name,
1636                &pairs,
1637                e.description.as_deref(),
1638                e.examples.as_deref(),
1639            )?;
1640        }
1641        for (name, schema) in &shared_structs {
1642            let root_idx: usize = *canonical_name_to_first_schema_idx
1643                .get(name)
1644                .expect("root schema index for shared struct");
1645            let root_schema: &JsonSchema = resolved_schemas.get(root_idx).expect("root schema");
1646            emit_default_functions_for_struct(&mut out, name, schema)?;
1647            emit_struct_derive_and_attrs(&mut out, name, schema)?;
1648            emit_struct_fields_with_resolver(
1649                root_schema,
1650                name,
1651                schema,
1652                &mut out,
1653                settings,
1654                Some(&key_to_canonical_name),
1655                mode,
1656                Some(&enum_values_to_name),
1657            )?;
1658            writeln!(out, "}}")?;
1659            writeln!(out)?;
1660        }
1661        {
1662            let result = maybe_prepend_btreemap_use(out.into_inner());
1663            let result = maybe_prepend_hash_set_use(result);
1664            #[cfg(feature = "uuid")]
1665            let result = maybe_prepend_uuid_use(result);
1666            result
1667        }
1668    };
1669
1670    let per_schema: Vec<Vec<u8>> = (0..schemas.len())
1671        .map(|schema_idx| {
1672            let mut local_structs: Vec<(String, JsonSchema)> = map
1673                .iter()
1674                .filter(|(_, (canonical_name, _, occs))| {
1675                    occs.len() == 1
1676                        && occs[0].0 == schema_idx
1677                        && !shared_names.contains(canonical_name)
1678                })
1679                .map(|(_, (name, schema, _))| (name.clone(), schema.clone()))
1680                .collect();
1681            let local_names: BTreeSet<String> =
1682                local_structs.iter().map(|(n, _)| n.clone()).collect();
1683            let mut deps: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
1684            for (name, schema) in &local_structs {
1685                let mut set: BTreeSet<String> = BTreeSet::new();
1686                for prop_schema in schema.properties.values() {
1687                    if prop_schema.is_object_with_properties() {
1688                        let prop_key = DedupeKey::from_schema(prop_schema, mode);
1689                        if let Some(cn) = key_to_canonical_name.get(&prop_key)
1690                            && (local_names.contains(cn) || shared_names.contains(cn))
1691                        {
1692                            set.insert(cn.clone());
1693                        }
1694                    }
1695                    if prop_schema.is_array_with_items()
1696                        && let Some(ref items) = prop_schema.items
1697                        && items.is_object_with_properties()
1698                    {
1699                        let item_key = DedupeKey::from_schema(items, mode);
1700                        if let Some(cn) = key_to_canonical_name.get(&item_key)
1701                            && (local_names.contains(cn) || shared_names.contains(cn))
1702                        {
1703                            set.insert(cn.clone());
1704                        }
1705                    }
1706                }
1707                deps.insert(name.clone(), set);
1708            }
1709            let order: Vec<String> = local_structs.iter().map(|(n, _)| n.clone()).collect();
1710            topo_sort_by_deps(&order, &deps, &mut local_structs);
1711
1712            let mut used_shared: BTreeSet<String> = BTreeSet::new();
1713            for (_, schema) in &local_structs {
1714                for prop_schema in schema.properties.values() {
1715                    if prop_schema.is_object_with_properties() {
1716                        let prop_key = DedupeKey::from_schema(prop_schema, mode);
1717                        if let Some(cn) = key_to_canonical_name.get(&prop_key)
1718                            && shared_names.contains(cn)
1719                        {
1720                            used_shared.insert(cn.clone());
1721                        }
1722                    }
1723                    if prop_schema.is_array_with_items()
1724                        && let Some(ref items) = prop_schema.items
1725                        && items.is_object_with_properties()
1726                    {
1727                        let item_key = DedupeKey::from_schema(items, mode);
1728                        if let Some(cn) = key_to_canonical_name.get(&item_key)
1729                            && shared_names.contains(cn)
1730                        {
1731                            used_shared.insert(cn.clone());
1732                        }
1733                    }
1734                    if let Some(values) = string_enum_or_const_values(prop_schema)
1735                        && let Some((enum_name, _, _)) = enum_values_to_name.get(&values)
1736                    {
1737                        used_shared.insert(enum_name.clone());
1738                    }
1739                }
1740            }
1741            let root_for_schema = collected
1742                .iter()
1743                .rev()
1744                .find(|(idx, _, _)| *idx == schema_idx)
1745                .map(|(_, _, s)| DedupeKey::from_schema(s, mode));
1746            if let Some(root_key) = root_for_schema
1747                && let Some(cn) = key_to_canonical_name.get(&root_key)
1748                && shared_names.contains(cn)
1749            {
1750                used_shared.insert(cn.clone());
1751            }
1752
1753            let mut buf = Cursor::new(Vec::new());
1754            writeln!(
1755                buf,
1756                "//! Generated by json-schema-rs. Do not edit manually."
1757            )
1758            .ok();
1759            writeln!(buf).ok();
1760            writeln!(buf, "use serde::{{Deserialize, Serialize}};").ok();
1761            for u in &used_shared {
1762                writeln!(buf, "pub use crate::{u};").ok();
1763            }
1764            if !used_shared.is_empty() {
1765                writeln!(buf).ok();
1766            }
1767            let root_schema: &JsonSchema = resolved_schemas
1768                .get(schema_idx)
1769                .expect("root schema for local emission");
1770            for (name, schema) in &local_structs {
1771                emit_default_functions_for_struct(&mut buf, name, schema).ok();
1772                emit_struct_derive_and_attrs(&mut buf, name, schema).ok();
1773                emit_struct_fields_with_resolver(
1774                    root_schema,
1775                    name,
1776                    schema,
1777                    &mut buf,
1778                    settings,
1779                    Some(&key_to_canonical_name),
1780                    mode,
1781                    Some(&enum_values_to_name),
1782                )
1783                .ok();
1784                writeln!(buf, "}}").ok();
1785                writeln!(buf).ok();
1786            }
1787            {
1788                let result = maybe_prepend_btreemap_use(buf.into_inner());
1789                let result = maybe_prepend_hash_set_use(result);
1790                #[cfg(feature = "uuid")]
1791                let result = maybe_prepend_uuid_use(result);
1792                result
1793            }
1794        })
1795        .collect();
1796
1797    Ok(GenerateRustOutput {
1798        shared: Some(shared_buffer),
1799        per_schema,
1800    })
1801}
1802
1803fn topo_sort_by_deps(
1804    order: &[String],
1805    deps: &BTreeMap<String, BTreeSet<String>>,
1806    v: &mut Vec<(String, JsonSchema)>,
1807) {
1808    let mut sorted: Vec<String> = Vec::new();
1809    let mut visited: BTreeSet<String> = BTreeSet::new();
1810    for name in order {
1811        visit_topo(name, deps, &mut visited, &mut sorted);
1812    }
1813    let name_to_pair: BTreeMap<String, (String, JsonSchema)> =
1814        v.drain(..).map(|(n, s)| (n.clone(), (n, s))).collect();
1815    for n in &sorted {
1816        if let Some(pair) = name_to_pair.get(n) {
1817            v.push(pair.clone());
1818        }
1819    }
1820}
1821
1822fn visit_topo(
1823    name: &str,
1824    deps: &BTreeMap<String, BTreeSet<String>>,
1825    visited: &mut BTreeSet<String>,
1826    out: &mut Vec<String>,
1827) {
1828    if visited.contains(name) {
1829        return;
1830    }
1831    visited.insert(name.to_string());
1832    if let Some(d) = deps.get(name) {
1833        for dep in d {
1834            visit_topo(dep, deps, visited, out);
1835        }
1836    }
1837    out.push(name.to_string());
1838}
1839
1840/// Emit a single Rust enum (unit variants). Pairs are (`json_value`, `variant_name`); serde rename when they differ.
1841/// Emits enum doc comment from description and examples when present.
1842fn emit_enum_from_pairs(
1843    out: &mut impl Write,
1844    name: &str,
1845    pairs: &[(String, String)],
1846    description: Option<&str>,
1847    examples: Option<&[serde_json::Value]>,
1848) -> CodeGenResult<()> {
1849    for line in doc_lines(description) {
1850        writeln!(out, "/// {line}")?;
1851    }
1852    for line in examples_doc_lines(examples) {
1853        writeln!(out, "/// {line}")?;
1854    }
1855    writeln!(
1856        out,
1857        "#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]"
1858    )?;
1859    writeln!(out, "pub enum {name} {{")?;
1860    for (value, variant_name) in pairs {
1861        if value != variant_name {
1862            let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
1863            writeln!(out, "    #[serde(rename = \"{escaped}\")]")?;
1864        }
1865        writeln!(out, "    {variant_name},")?;
1866    }
1867    writeln!(out, "}}")?;
1868    writeln!(out)?;
1869    Ok(())
1870}
1871
1872/// Emit a single anyOf enum (union) to `out`.
1873/// We do not derive `ToJsonSchema` (macro supports only unit enums) or PartialEq/Eq (variant structs may not implement them).
1874fn emit_anyof_enum(out: &mut impl Write, a: &AnyOfEnumToEmit) -> CodeGenResult<()> {
1875    writeln!(out, "#[derive(Debug, Clone, Serialize, Deserialize)]")?;
1876    writeln!(out, "pub enum {} {{", a.name)?;
1877    for (variant_name, ty) in &a.variants {
1878        writeln!(out, "    {variant_name}({ty}),")?;
1879    }
1880    writeln!(out, "}}")?;
1881    writeln!(out)?;
1882    Ok(())
1883}
1884
1885/// Emit a single oneOf enum (union) to `out`.
1886/// We do not derive `ToJsonSchema` (macro supports only unit enums) or PartialEq/Eq (variant structs may not implement them).
1887fn emit_oneof_enum(out: &mut impl Write, a: &OneOfEnumToEmit) -> CodeGenResult<()> {
1888    writeln!(out, "#[derive(Debug, Clone, Serialize, Deserialize)]")?;
1889    writeln!(out, "pub enum {} {{", a.name)?;
1890    for (variant_name, ty) in &a.variants {
1891        writeln!(out, "    {variant_name}({ty}),")?;
1892    }
1893    writeln!(out, "}}")?;
1894    writeln!(out)?;
1895    Ok(())
1896}
1897
1898/// Emits `#[serde(default)]` or `#[serde(default = "fn")]` when the property has a default value.
1899/// Default functions are emitted at module level by `emit_default_functions_for_struct`.
1900fn emit_default_attr(
1901    out: &mut impl Write,
1902    struct_name: &str,
1903    field_name: &str,
1904    prop_schema: &JsonSchema,
1905    ty: &str,
1906    is_required: bool,
1907) -> CodeGenResult<()> {
1908    let Some(ref value) = prop_schema.default_value else {
1909        return Ok(());
1910    };
1911    let is_optional: bool = !is_required;
1912    if json_value_equals_rust_type_default(value, ty, is_optional) {
1913        writeln!(out, "    #[serde(default)]")?;
1914        return Ok(());
1915    }
1916    let Some(_expr) = json_default_to_rust_expr(value, ty, is_optional) else {
1917        return Ok(());
1918    };
1919    let fn_name: String = default_function_name(struct_name, field_name);
1920    writeln!(out, "    #[serde(default = \"{fn_name}\")]")?;
1921    Ok(())
1922}
1923
1924/// Emits module-level default functions for all properties of the struct that have a custom default value.
1925fn emit_default_functions_for_struct(
1926    out: &mut impl Write,
1927    struct_name: &str,
1928    schema: &JsonSchema,
1929) -> CodeGenResult<()> {
1930    for (key, prop_schema) in &schema.properties {
1931        let Some(ref value) = prop_schema.default_value else {
1932            continue;
1933        };
1934        let field_name = sanitize_field_name(key);
1935        let is_required = schema.is_required(key);
1936        let is_optional = !is_required;
1937        let ty: String = if prop_schema.is_string() {
1938            if is_required {
1939                "String".to_string()
1940            } else {
1941                "Option<String>".to_string()
1942            }
1943        } else if prop_schema.is_integer() || prop_schema.is_number() {
1944            let inner = rust_numeric_type_for_schema(prop_schema);
1945            if is_required {
1946                inner
1947            } else {
1948                format!("Option<{inner}>")
1949            }
1950        } else if prop_schema.is_boolean() {
1951            if is_required {
1952                "bool".to_string()
1953            } else {
1954                "Option<bool>".to_string()
1955            }
1956        } else {
1957            continue;
1958        };
1959        if json_value_equals_rust_type_default(value, &ty, is_optional) {
1960            continue;
1961        }
1962        let Some(expr) = json_default_to_rust_expr(value, &ty, is_optional) else {
1963            continue;
1964        };
1965        let fn_name = default_function_name(struct_name, &field_name);
1966        writeln!(out, "fn {fn_name}() -> {ty} {{ {expr} }}")?;
1967        writeln!(out)?;
1968    }
1969    Ok(())
1970}
1971
1972/// Emit struct fields; when resolver is Some (dedupe mode), use canonical type names for nested objects.
1973#[expect(clippy::too_many_lines, clippy::too_many_arguments)]
1974fn emit_struct_fields_with_resolver(
1975    root: &JsonSchema,
1976    struct_name: &str,
1977    schema: &JsonSchema,
1978    out: &mut impl Write,
1979    settings: &CodeGenSettings,
1980    key_to_name: Option<&BTreeMap<DedupeKey, String>>,
1981    mode: DedupeMode,
1982    enum_values_to_name: Option<&EnumValuesToNameMap>,
1983) -> CodeGenResult<()> {
1984    let enum_names_simple: Option<BTreeMap<Vec<String>, String>> = enum_values_to_name.map(|m| {
1985        m.iter()
1986            .map(|(k, (n, _, _))| (k.clone(), n.clone()))
1987            .collect()
1988    });
1989    for (key, prop_schema) in &schema.properties {
1990        let (prop_schema_effective, _) = resolve_ref_for_codegen(root, prop_schema, Some(key))?;
1991        let prop_schema: &JsonSchema = &prop_schema_effective;
1992
1993        for line in doc_lines(prop_schema.description.as_deref()) {
1994            writeln!(out, "    /// {line}")?;
1995        }
1996        emit_deprecated_attr(out, prop_schema)?;
1997        let field_name = sanitize_field_name(key);
1998        let needs_rename = field_name != *key;
1999
2000        if let Some(values) = string_enum_or_const_values(prop_schema) {
2001            let enum_name: &String = enum_values_to_name
2002                .and_then(|m| m.get(&values).map(|(n, _, _)| n))
2003                .expect("enum name for string enum");
2004            let ty = if schema.is_required(key) {
2005                enum_name.clone()
2006            } else {
2007                format!("Option<{enum_name}>")
2008            };
2009            if needs_rename {
2010                writeln!(out, "    #[serde(rename = \"{key}\")]")?;
2011            }
2012            writeln!(out, "    pub {field_name}: {ty},")?;
2013        } else if prop_schema.is_string()
2014            || (prop_schema
2015                .enum_values
2016                .as_ref()
2017                .is_some_and(|v| !v.is_empty())
2018                && !prop_schema.is_string_enum())
2019        {
2020            #[cfg(feature = "uuid")]
2021            if prop_schema.is_string() && prop_schema.format.as_deref() == Some("uuid") {
2022                let ty = if schema.is_required(key) {
2023                    "Uuid".to_string()
2024                } else {
2025                    "Option<Uuid>".to_string()
2026                };
2027                if needs_rename {
2028                    writeln!(out, "    #[serde(rename = \"{key}\")]")?;
2029                }
2030                writeln!(out, "    pub {field_name}: {ty},")?;
2031                continue;
2032            }
2033            // String type, or non-string/mixed enum fallback per design.
2034            let ty = if schema.is_required(key) {
2035                "String".to_string()
2036            } else {
2037                "Option<String>".to_string()
2038            };
2039            if needs_rename {
2040                writeln!(out, "    #[serde(rename = \"{key}\")]")?;
2041            }
2042            if prop_schema.min_length.is_some()
2043                || prop_schema.max_length.is_some()
2044                || prop_schema.pattern.is_some()
2045            {
2046                let mut attrs: Vec<String> = Vec::new();
2047                if let Some(n) = prop_schema.min_length {
2048                    attrs.push(format!("min_length = {n}"));
2049                }
2050                if let Some(n) = prop_schema.max_length {
2051                    attrs.push(format!("max_length = {n}"));
2052                }
2053                if let Some(ref p) = prop_schema.pattern {
2054                    let escaped = p.replace('\\', "\\\\").replace('"', "\\\"");
2055                    attrs.push(format!("pattern = \"{escaped}\""));
2056                }
2057                writeln!(out, "    #[json_schema({})]", attrs.join(", "))?;
2058            }
2059            emit_default_attr(
2060                out,
2061                struct_name,
2062                &field_name,
2063                prop_schema,
2064                &ty,
2065                schema.is_required(key),
2066            )?;
2067            writeln!(out, "    pub {field_name}: {ty},")?;
2068        } else if prop_schema.is_integer() || prop_schema.is_number() {
2069            let inner: String = rust_numeric_type_for_schema(prop_schema);
2070            let ty = if schema.is_required(key) {
2071                inner
2072            } else {
2073                format!("Option<{inner}>")
2074            };
2075            if needs_rename {
2076                writeln!(out, "    #[serde(rename = \"{key}\")]")?;
2077            }
2078            emit_default_attr(
2079                out,
2080                struct_name,
2081                &field_name,
2082                prop_schema,
2083                &ty,
2084                schema.is_required(key),
2085            )?;
2086            writeln!(out, "    pub {field_name}: {ty},")?;
2087        } else if prop_schema.is_boolean() {
2088            let ty: String = if schema.is_required(key) {
2089                "bool".to_string()
2090            } else {
2091                "Option<bool>".to_string()
2092            };
2093            if needs_rename {
2094                writeln!(out, "    #[serde(rename = \"{key}\")]")?;
2095            }
2096            emit_default_attr(
2097                out,
2098                struct_name,
2099                &field_name,
2100                prop_schema,
2101                &ty,
2102                schema.is_required(key),
2103            )?;
2104            writeln!(out, "    pub {field_name}: {ty},")?;
2105        } else if prop_schema.is_array_with_items() {
2106            let item_schema: &JsonSchema = prop_schema
2107                .items
2108                .as_ref()
2109                .expect("array with items")
2110                .as_ref();
2111            let inner: String = rust_type_for_item_schema(
2112                root,
2113                item_schema,
2114                Some(key),
2115                enum_names_simple.as_ref(),
2116                key_to_name,
2117                settings,
2118                mode,
2119            )?;
2120            let use_hash_set: bool =
2121                prop_schema.unique_items == Some(true) && item_schema_is_hashable(item_schema);
2122            let container: &str = if use_hash_set { "HashSet" } else { "Vec" };
2123            let ty = if schema.is_required(key) {
2124                format!("{container}<{inner}>")
2125            } else {
2126                format!("Option<{container}<{inner}>>")
2127            };
2128            if needs_rename {
2129                writeln!(out, "    #[serde(rename = \"{key}\")]")?;
2130            }
2131            if prop_schema.min_items.is_some() || prop_schema.max_items.is_some() {
2132                let mut attrs: Vec<String> = Vec::new();
2133                if let Some(n) = prop_schema.min_items {
2134                    attrs.push(format!("min_items = {n}"));
2135                }
2136                if let Some(n) = prop_schema.max_items {
2137                    attrs.push(format!("max_items = {n}"));
2138                }
2139                writeln!(out, "    #[json_schema({})]", attrs.join(", "))?;
2140            }
2141            writeln!(out, "    pub {field_name}: {ty},")?;
2142        } else if prop_schema.is_object_with_properties() {
2143            let nested_name: String = if let Some(m) = key_to_name {
2144                let prop_key = DedupeKey::from_schema(prop_schema, mode);
2145                m.get(&prop_key).cloned().unwrap_or_else(|| {
2146                    struct_name_from(prop_schema.title.as_deref(), Some(key), false, settings)
2147                })
2148            } else {
2149                struct_name_from(prop_schema.title.as_deref(), Some(key), false, settings)
2150            };
2151            let ty = if schema.is_required(key) {
2152                nested_name.clone()
2153            } else {
2154                format!("Option<{nested_name}>")
2155            };
2156            if needs_rename {
2157                writeln!(out, "    #[serde(rename = \"{key}\")]")?;
2158            }
2159            writeln!(out, "    pub {field_name}: {ty},")?;
2160        }
2161    }
2162    if let Some(AdditionalProperties::Schema(sub)) = &schema.additional_properties {
2163        let value_ty: String = rust_type_for_item_schema(
2164            root,
2165            sub,
2166            Some("additional"),
2167            enum_names_simple.as_ref(),
2168            key_to_name,
2169            settings,
2170            mode,
2171        )?;
2172        writeln!(out, "    #[serde(default)]")?;
2173        writeln!(out, "    pub additional: BTreeMap<String, {value_ty}>,")?;
2174    }
2175    Ok(())
2176}
2177
2178/// Emit a single struct's fields to `out`.
2179#[expect(clippy::too_many_lines, clippy::too_many_arguments)]
2180fn emit_struct_fields(
2181    root: &JsonSchema,
2182    struct_name: &str,
2183    schema: &JsonSchema,
2184    out: &mut impl Write,
2185    settings: &CodeGenSettings,
2186    enum_values_to_name: Option<&BTreeMap<Vec<String>, String>>,
2187    _anyof_enums: Option<&[AnyOfEnumToEmit]>,
2188    _oneof_enums: Option<&[OneOfEnumToEmit]>,
2189) -> CodeGenResult<()> {
2190    for (key, prop_schema) in &schema.properties {
2191        let (prop_schema_effective, _) = resolve_ref_for_codegen(root, prop_schema, Some(key))?;
2192        let prop_schema: &JsonSchema = &prop_schema_effective;
2193
2194        for line in doc_lines(prop_schema.description.as_deref()) {
2195            writeln!(out, "    /// {line}")?;
2196        }
2197        emit_deprecated_attr(out, prop_schema)?;
2198        let field_name = sanitize_field_name(key);
2199        let needs_rename = field_name != *key;
2200
2201        if prop_schema.any_of.as_ref().is_some_and(|v| !v.is_empty()) {
2202            let enum_name = sanitize_struct_name(key) + "AnyOf";
2203            let ty = if schema.is_required(key) {
2204                enum_name.clone()
2205            } else {
2206                format!("Option<{enum_name}>")
2207            };
2208            if needs_rename {
2209                writeln!(out, "    #[serde(rename = \"{key}\")]")?;
2210            }
2211            writeln!(out, "    pub {field_name}: {ty},")?;
2212        } else if prop_schema.one_of.as_ref().is_some_and(|v| !v.is_empty()) {
2213            let enum_name = sanitize_struct_name(key) + "OneOf";
2214            let ty = if schema.is_required(key) {
2215                enum_name.clone()
2216            } else {
2217                format!("Option<{enum_name}>")
2218            };
2219            if needs_rename {
2220                writeln!(out, "    #[serde(rename = \"{key}\")]")?;
2221            }
2222            writeln!(out, "    pub {field_name}: {ty},")?;
2223        } else if let Some(values) = string_enum_or_const_values(prop_schema) {
2224            let enum_name: &String = enum_values_to_name
2225                .and_then(|m| m.get(&values))
2226                .expect("enum name for string enum");
2227            let ty = if schema.is_required(key) {
2228                enum_name.clone()
2229            } else {
2230                format!("Option<{enum_name}>")
2231            };
2232            if needs_rename {
2233                writeln!(out, "    #[serde(rename = \"{key}\")]")?;
2234            }
2235            writeln!(out, "    pub {field_name}: {ty},")?;
2236        } else if prop_schema.is_string()
2237            || (prop_schema
2238                .enum_values
2239                .as_ref()
2240                .is_some_and(|v| !v.is_empty())
2241                && !prop_schema.is_string_enum())
2242        {
2243            #[cfg(feature = "uuid")]
2244            if prop_schema.is_string() && prop_schema.format.as_deref() == Some("uuid") {
2245                let ty = if schema.is_required(key) {
2246                    "Uuid".to_string()
2247                } else {
2248                    "Option<Uuid>".to_string()
2249                };
2250                if needs_rename {
2251                    writeln!(out, "    #[serde(rename = \"{key}\")]")?;
2252                }
2253                writeln!(out, "    pub {field_name}: {ty},")?;
2254                continue;
2255            }
2256            // String type, or non-string/mixed enum fallback per design.
2257            let ty = if schema.is_required(key) {
2258                "String".to_string()
2259            } else {
2260                "Option<String>".to_string()
2261            };
2262            if needs_rename {
2263                writeln!(out, "    #[serde(rename = \"{key}\")]")?;
2264            }
2265            if prop_schema.min_length.is_some()
2266                || prop_schema.max_length.is_some()
2267                || prop_schema.pattern.is_some()
2268            {
2269                let mut attrs: Vec<String> = Vec::new();
2270                if let Some(n) = prop_schema.min_length {
2271                    attrs.push(format!("min_length = {n}"));
2272                }
2273                if let Some(n) = prop_schema.max_length {
2274                    attrs.push(format!("max_length = {n}"));
2275                }
2276                if let Some(ref p) = prop_schema.pattern {
2277                    let escaped = p.replace('\\', "\\\\").replace('"', "\\\"");
2278                    attrs.push(format!("pattern = \"{escaped}\""));
2279                }
2280                writeln!(out, "    #[json_schema({})]", attrs.join(", "))?;
2281            }
2282            emit_default_attr(
2283                out,
2284                struct_name,
2285                &field_name,
2286                prop_schema,
2287                &ty,
2288                schema.is_required(key),
2289            )?;
2290            writeln!(out, "    pub {field_name}: {ty},")?;
2291        } else if prop_schema.is_integer() || prop_schema.is_number() {
2292            let inner: String = rust_numeric_type_for_schema(prop_schema);
2293            let ty = if schema.is_required(key) {
2294                inner
2295            } else {
2296                format!("Option<{inner}>")
2297            };
2298            if needs_rename {
2299                writeln!(out, "    #[serde(rename = \"{key}\")]")?;
2300            }
2301            emit_default_attr(
2302                out,
2303                struct_name,
2304                &field_name,
2305                prop_schema,
2306                &ty,
2307                schema.is_required(key),
2308            )?;
2309            writeln!(out, "    pub {field_name}: {ty},")?;
2310        } else if prop_schema.is_boolean() {
2311            let ty: String = if schema.is_required(key) {
2312                "bool".to_string()
2313            } else {
2314                "Option<bool>".to_string()
2315            };
2316            if needs_rename {
2317                writeln!(out, "    #[serde(rename = \"{key}\")]")?;
2318            }
2319            emit_default_attr(
2320                out,
2321                struct_name,
2322                &field_name,
2323                prop_schema,
2324                &ty,
2325                schema.is_required(key),
2326            )?;
2327            writeln!(out, "    pub {field_name}: {ty},")?;
2328        } else if prop_schema.is_array_with_items() {
2329            let item_schema: &JsonSchema = prop_schema
2330                .items
2331                .as_ref()
2332                .expect("array with items")
2333                .as_ref();
2334            let inner: String = rust_type_for_item_schema(
2335                root,
2336                item_schema,
2337                Some(key),
2338                enum_values_to_name,
2339                None,
2340                settings,
2341                DedupeMode::Full,
2342            )?;
2343            let use_hash_set: bool =
2344                prop_schema.unique_items == Some(true) && item_schema_is_hashable(item_schema);
2345            let container: &str = if use_hash_set { "HashSet" } else { "Vec" };
2346            let ty = if schema.is_required(key) {
2347                format!("{container}<{inner}>")
2348            } else {
2349                format!("Option<{container}<{inner}>>")
2350            };
2351            if needs_rename {
2352                writeln!(out, "    #[serde(rename = \"{key}\")]")?;
2353            }
2354            if prop_schema.min_items.is_some() || prop_schema.max_items.is_some() {
2355                let mut attrs: Vec<String> = Vec::new();
2356                if let Some(n) = prop_schema.min_items {
2357                    attrs.push(format!("min_items = {n}"));
2358                }
2359                if let Some(n) = prop_schema.max_items {
2360                    attrs.push(format!("max_items = {n}"));
2361                }
2362                writeln!(out, "    #[json_schema({})]", attrs.join(", "))?;
2363            }
2364            writeln!(out, "    pub {field_name}: {ty},")?;
2365        } else if prop_schema.is_object_with_properties() {
2366            let nested_name: String =
2367                struct_name_from(prop_schema.title.as_deref(), Some(key), false, settings);
2368            let ty = if schema.is_required(key) {
2369                nested_name.clone()
2370            } else {
2371                format!("Option<{nested_name}>")
2372            };
2373            if needs_rename {
2374                writeln!(out, "    #[serde(rename = \"{key}\")]")?;
2375            }
2376            writeln!(out, "    pub {field_name}: {ty},")?;
2377        }
2378    }
2379    if let Some(AdditionalProperties::Schema(sub)) = &schema.additional_properties {
2380        let value_ty: String = rust_type_for_item_schema(
2381            root,
2382            sub,
2383            Some("additional"),
2384            enum_values_to_name,
2385            None,
2386            settings,
2387            settings.dedupe_mode,
2388        )?;
2389        writeln!(out, "    #[serde(default)]")?;
2390        writeln!(out, "    pub additional: BTreeMap<String, {value_ty}>,")?;
2391    }
2392    Ok(())
2393}
2394
2395/// Emit Rust source from a parsed schema to `out`. Used by [`RustBackend::generate`].
2396fn emit_rust(
2397    schema: &JsonSchema,
2398    out: &mut impl Write,
2399    settings: &CodeGenSettings,
2400) -> CodeGenResult<()> {
2401    let root_unresolved = resolve_all_of_for_codegen(schema)?;
2402    let (root, root_from_key) = resolve_ref_for_codegen(schema, &root_unresolved, None)?;
2403    if root.any_of.as_ref().is_some_and(std::vec::Vec::is_empty) {
2404        return Err(CodeGenError::AnyOfEmpty);
2405    }
2406    if root.one_of.as_ref().is_some_and(std::vec::Vec::is_empty) {
2407        return Err(CodeGenError::OneOfEmpty);
2408    }
2409    let roots_for_structs: Vec<JsonSchema> = if root.one_of.as_ref().is_some_and(|v| !v.is_empty())
2410    {
2411        root.one_of
2412            .as_ref()
2413            .unwrap()
2414            .iter()
2415            .map(resolve_all_of_for_codegen)
2416            .collect::<CodeGenResult<Vec<_>>>()?
2417    } else if root.any_of.as_ref().is_some_and(|v| !v.is_empty()) {
2418        root.any_of
2419            .as_ref()
2420            .unwrap()
2421            .iter()
2422            .map(resolve_all_of_for_codegen)
2423            .collect::<CodeGenResult<Vec<_>>>()?
2424    } else {
2425        if !root.is_object_with_properties() {
2426            return Err(CodeGenError::RootNotObject);
2427        }
2428        vec![root.clone()]
2429    };
2430
2431    let enums: Vec<EnumToEmit> = collect_enums(schema, &root, settings)?;
2432    let enum_values_to_name: BTreeMap<Vec<String>, String> = enums
2433        .iter()
2434        .map(|e| (e.values.clone(), e.name.clone()))
2435        .collect();
2436
2437    let anyof_enums: Vec<AnyOfEnumToEmit> =
2438        collect_anyof_enums(schema, &root, settings, &enum_values_to_name)?;
2439    let oneof_enums: Vec<OneOfEnumToEmit> =
2440        collect_oneof_enums(schema, &root, settings, &enum_values_to_name)?;
2441
2442    let mut structs: Vec<StructToEmit> = Vec::new();
2443    let mut seen: BTreeSet<String> = BTreeSet::new();
2444    let root_is_anyof = root.any_of.as_ref().is_some_and(|v| !v.is_empty());
2445    let root_is_oneof = root.one_of.as_ref().is_some_and(|v| !v.is_empty());
2446    for (i, r) in roots_for_structs.iter().enumerate() {
2447        let from_key: Option<String> = if root_is_anyof || root_is_oneof {
2448            Some(format!("Root_Variant{i}"))
2449        } else {
2450            root_from_key.clone()
2451        };
2452        collect_structs(
2453            schema,
2454            r,
2455            from_key.as_deref(),
2456            &mut structs,
2457            &mut seen,
2458            settings,
2459        )?;
2460    }
2461
2462    writeln!(
2463        out,
2464        "//! Generated by json-schema-rs. Do not edit manually."
2465    )?;
2466    writeln!(out)?;
2467    writeln!(out, "use serde::{{Deserialize, Serialize}};")?;
2468    writeln!(out)?;
2469
2470    for e in &enums {
2471        let pairs: Vec<(String, String)> = enum_variant_names_with_collision_resolution(&e.values);
2472        emit_enum_from_pairs(
2473            out,
2474            &e.name,
2475            &pairs,
2476            e.description.as_deref(),
2477            e.examples.as_deref(),
2478        )?;
2479    }
2480
2481    for a in &anyof_enums {
2482        emit_anyof_enum(out, a)?;
2483    }
2484
2485    for o in &oneof_enums {
2486        emit_oneof_enum(out, o)?;
2487    }
2488
2489    for st in &structs {
2490        emit_default_functions_for_struct(out, &st.name, &st.schema)?;
2491        emit_struct_derive_and_attrs(out, &st.name, &st.schema)?;
2492        emit_struct_fields(
2493            schema,
2494            &st.name,
2495            &st.schema,
2496            out,
2497            settings,
2498            Some(&enum_values_to_name),
2499            Some(&anyof_enums),
2500            Some(&oneof_enums),
2501        )?;
2502        writeln!(out, "}}")?;
2503        writeln!(out)?;
2504    }
2505
2506    Ok(())
2507}
2508
2509/// Generate Rust source from one or more parsed schemas.
2510///
2511/// Callers must pass `settings` (use [`CodeGenSettings::builder`] and call [`CodeGenSettingsBuilder::build`]
2512/// for all-default settings). Returns [`GenerateRustOutput`] with optional shared buffer and one buffer per schema.
2513/// Root of each schema must have `type: "object"` and non-empty `properties`.
2514///
2515/// # Errors
2516///
2517/// Returns [`CodeGenError::RootNotObject`] if a root schema is not an object with properties.
2518/// Returns [`CodeGenError::Io`] on write failure.
2519/// Returns [`CodeGenError::Batch`] with index when one schema in the batch fails.
2520pub fn generate_rust(
2521    schemas: &[JsonSchema],
2522    settings: &CodeGenSettings,
2523) -> CodeGenResult<GenerateRustOutput> {
2524    RustBackend.generate(schemas, settings)
2525}
2526
2527#[cfg(test)]
2528mod tests {
2529    use super::CodeGenError;
2530    use super::{CodeGenBackend, RustBackend, generate_rust, merge_all_of};
2531    use crate::code_gen::settings::{CodeGenSettings, DedupeMode, ModelNameSource};
2532    use crate::json_schema::JsonSchema;
2533
2534    fn default_settings() -> CodeGenSettings {
2535        CodeGenSettings::builder().build()
2536    }
2537
2538    #[test]
2539    fn root_not_object_errors() {
2540        let schema: JsonSchema = JsonSchema::default();
2541        let settings: CodeGenSettings = default_settings();
2542        let actual = generate_rust(&[schema], &settings).unwrap_err();
2543        assert!(matches!(actual, CodeGenError::Batch { index: 0, .. }));
2544        if let CodeGenError::Batch { source, .. } = actual {
2545            assert!(matches!(*source, CodeGenError::RootNotObject));
2546        }
2547    }
2548
2549    #[test]
2550    fn root_object_empty_properties_errors() {
2551        let schema: JsonSchema = JsonSchema {
2552            type_: Some("object".to_string()),
2553            ..Default::default()
2554        };
2555        let settings: CodeGenSettings = default_settings();
2556        let actual = generate_rust(&[schema], &settings).unwrap_err();
2557        assert!(matches!(actual, CodeGenError::Batch { index: 0, .. }));
2558        if let CodeGenError::Batch { source, .. } = actual {
2559            assert!(matches!(*source, CodeGenError::RootNotObject));
2560        }
2561    }
2562
2563    #[test]
2564    fn schema_with_id_emits_id_attribute() {
2565        let json = r#"{"$id":"http://example.com/schema","type":"object","properties":{"name":{"type":"string"}}}"#;
2566        let schema: JsonSchema = serde_json::from_str(json).unwrap();
2567        let settings: CodeGenSettings = default_settings();
2568        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2569        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
2570        let expected = concat!(
2571            "//! Generated by json-schema-rs. Do not edit manually.\n\n",
2572            "use serde::{Deserialize, Serialize};\n\n",
2573            "#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\n",
2574            "#[json_schema(id = \"http://example.com/schema\")]\n",
2575            "pub struct Root {\n    pub name: Option<String>,\n}\n\n"
2576        );
2577        assert_eq!(expected, actual);
2578    }
2579
2580    #[test]
2581    fn additional_properties_false_emits_deny_unknown_fields() {
2582        let json = r#"{"type":"object","properties":{"name":{"type":"string"}},"additionalProperties":false}"#;
2583        let schema: JsonSchema = serde_json::from_str(json).unwrap();
2584        let settings: CodeGenSettings = default_settings();
2585        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2586        let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
2587        let expected = concat!(
2588            "//! Generated by json-schema-rs. Do not edit manually.\n\n",
2589            "use serde::{Deserialize, Serialize};\n\n",
2590            "#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\n",
2591            "#[serde(deny_unknown_fields)]\n",
2592            "pub struct Root {\n    pub name: Option<String>,\n}\n\n"
2593        );
2594        assert_eq!(expected, actual);
2595    }
2596
2597    #[test]
2598    fn deprecated_property_emits_deprecated_attr() {
2599        let json =
2600            r#"{"type":"object","properties":{"legacy":{"type":"string","deprecated":true}}}"#;
2601        let schema: JsonSchema = serde_json::from_str(json).unwrap();
2602        let settings: CodeGenSettings = default_settings();
2603        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2604        let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
2605        let expected = concat!(
2606            "//! Generated by json-schema-rs. Do not edit manually.\n\n",
2607            "use serde::{Deserialize, Serialize};\n\n",
2608            "#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\n",
2609            "pub struct Root {\n",
2610            "    #[deprecated]\n",
2611            "    pub legacy: Option<String>,\n",
2612            "}\n\n"
2613        );
2614        assert_eq!(expected, actual);
2615    }
2616
2617    #[test]
2618    fn deprecated_property_with_description_emits_deprecated_with_message() {
2619        let json = r#"{"type":"object","properties":{"old":{"type":"string","deprecated":true,"description":"Use 'new' instead"}}}"#;
2620        let schema: JsonSchema = serde_json::from_str(json).unwrap();
2621        let settings: CodeGenSettings = default_settings();
2622        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2623        let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
2624        let expected = concat!(
2625            "//! Generated by json-schema-rs. Do not edit manually.\n\n",
2626            "use serde::{Deserialize, Serialize};\n\n",
2627            "#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\n",
2628            "pub struct Root {\n",
2629            "    /// Use 'new' instead\n",
2630            "    #[deprecated = \"Use 'new' instead\"]\n",
2631            "    pub old: Option<String>,\n",
2632            "}\n\n"
2633        );
2634        assert_eq!(expected, actual);
2635    }
2636
2637    #[test]
2638    fn deprecated_false_emits_no_attr() {
2639        let json = r#"{"type":"object","properties":{"x":{"type":"string","deprecated":false}}}"#;
2640        let schema: JsonSchema = serde_json::from_str(json).unwrap();
2641        let settings: CodeGenSettings = default_settings();
2642        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2643        let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
2644        let expected = concat!(
2645            "//! Generated by json-schema-rs. Do not edit manually.\n\n",
2646            "use serde::{Deserialize, Serialize};\n\n",
2647            "#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\n",
2648            "pub struct Root {\n",
2649            "    pub x: Option<String>,\n",
2650            "}\n\n"
2651        );
2652        assert_eq!(expected, actual);
2653    }
2654
2655    #[test]
2656    fn additional_properties_schema_emits_map_field() {
2657        let json = r#"{"type":"object","properties":{"name":{"type":"string"}},"additionalProperties":{"type":"string"}}"#;
2658        let schema: JsonSchema = serde_json::from_str(json).unwrap();
2659        let settings: CodeGenSettings = default_settings();
2660        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2661        let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
2662        let expected = concat!(
2663            "//! Generated by json-schema-rs. Do not edit manually.\n\n",
2664            "use serde::{Deserialize, Serialize};\n",
2665            "use std::collections::BTreeMap;\n\n",
2666            "#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\n",
2667            "pub struct Root {\n",
2668            "    pub name: Option<String>,\n",
2669            "    #[serde(default)]\n",
2670            "    pub additional: BTreeMap<String, String>,\n",
2671            "}\n\n"
2672        );
2673        assert_eq!(expected, actual);
2674    }
2675
2676    #[test]
2677    fn single_string_property() {
2678        let json = r#"{"type":"object","properties":{"name":{"type":"string"}}}"#;
2679        let schema: JsonSchema = serde_json::from_str(json).unwrap();
2680        let settings: CodeGenSettings = default_settings();
2681        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2682        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
2683        let expected = concat!(
2684            "//! Generated by json-schema-rs. Do not edit manually.\n\n",
2685            "use serde::{Deserialize, Serialize};\n\n",
2686            "#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\n",
2687            "pub struct Root {\n    pub name: Option<String>,\n}\n\n"
2688        );
2689        assert_eq!(expected, actual);
2690    }
2691
2692    #[test]
2693    fn string_property_with_pattern_emits_attribute() {
2694        let json =
2695            r#"{"type":"object","properties":{"name":{"type":"string","pattern":"^[a-z]+$"}}}"#;
2696        let schema: JsonSchema = serde_json::from_str(json).unwrap();
2697        let settings: CodeGenSettings = default_settings();
2698        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2699        let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
2700        let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
2701
2702use serde::{Deserialize, Serialize};
2703
2704#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2705pub struct Root {
2706    #[json_schema(pattern = "^[a-z]+$")]
2707    pub name: Option<String>,
2708}
2709
2710"#;
2711        assert_eq!(expected, actual);
2712    }
2713
2714    #[test]
2715    fn required_field_emits_without_option() {
2716        let json = r#"{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]}"#;
2717        let schema: JsonSchema = serde_json::from_str(json).unwrap();
2718        let settings: CodeGenSettings = default_settings();
2719        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2720        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
2721        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
2722
2723use serde::{Deserialize, Serialize};
2724
2725#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2726pub struct Root {
2727    pub id: String,
2728}
2729
2730";
2731        assert_eq!(expected, actual);
2732    }
2733
2734    #[test]
2735    fn ref_to_defs_object_emits_named_struct_and_field_type() {
2736        let json = r##"{
2737  "$defs": {
2738    "Address": {
2739      "type": "object",
2740      "properties": { "city": { "type": "string" } },
2741      "required": ["city"]
2742    }
2743  },
2744  "type": "object",
2745  "properties": { "address": { "$ref": "#/$defs/Address" } },
2746  "required": ["address"]
2747}"##;
2748        let schema: JsonSchema = serde_json::from_str(json).unwrap();
2749        let settings: CodeGenSettings = default_settings();
2750        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2751        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
2752        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
2753
2754use serde::{Deserialize, Serialize};
2755
2756#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2757pub struct Address {
2758    pub city: String,
2759}
2760
2761#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2762pub struct Root {
2763    pub address: Address,
2764}
2765
2766";
2767        assert_eq!(expected, actual);
2768    }
2769
2770    #[test]
2771    fn ref_to_missing_defs_returns_ref_resolution_error() {
2772        let json = r##"{
2773  "type": "object",
2774  "properties": { "x": { "$ref": "#/$defs/Missing" } }
2775}"##;
2776        let schema: JsonSchema = serde_json::from_str(json).unwrap();
2777        let settings: CodeGenSettings = default_settings();
2778        let actual: super::CodeGenResult<super::GenerateRustOutput> =
2779            generate_rust(&[schema], &settings);
2780        assert!(
2781            matches!(
2782                actual,
2783                Err(CodeGenError::RefResolution { ref ref_str, ref reason })
2784                if ref_str == "#/$defs/Missing" && reason.contains("DefsMissing")
2785            ),
2786            "expected RefResolution with DefsMissing, got: {actual:?}"
2787        );
2788    }
2789
2790    #[test]
2791    fn ref_cycle_in_defs_returns_ref_resolution_error() {
2792        let json = r##"{
2793  "$defs": {
2794    "A": { "$ref": "#/$defs/B" },
2795    "B": { "$ref": "#/$defs/A" }
2796  },
2797  "type": "object",
2798  "properties": { "x": { "$ref": "#/$defs/A" } }
2799}"##;
2800        let schema: JsonSchema = serde_json::from_str(json).unwrap();
2801        let settings: CodeGenSettings = default_settings();
2802        let actual: super::CodeGenResult<super::GenerateRustOutput> =
2803            generate_rust(&[schema], &settings);
2804        assert!(
2805            matches!(
2806                actual,
2807                Err(CodeGenError::RefResolution { ref ref_str, ref reason })
2808                if ref_str == "#/$defs/A" && reason.contains("RefCycle")
2809            ),
2810            "expected RefResolution with RefCycle, got: {actual:?}"
2811        );
2812    }
2813
2814    #[test]
2815    fn required_enum_property_emits_enum_and_struct() {
2816        let json = r#"{"type":"object","properties":{"status":{"enum":["open","closed"]}},"required":["status"]}"#;
2817        let schema: JsonSchema = serde_json::from_str(json).unwrap();
2818        let settings: CodeGenSettings = default_settings();
2819        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2820        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
2821        let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
2822
2823use serde::{Deserialize, Serialize};
2824
2825#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2826pub enum Status {
2827    #[serde(rename = "closed")]
2828    Closed,
2829    #[serde(rename = "open")]
2830    Open,
2831}
2832
2833#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2834pub struct Root {
2835    pub status: Status,
2836}
2837
2838"#;
2839        assert_eq!(expected, actual);
2840    }
2841
2842    #[test]
2843    fn const_string_property_emits_single_variant_enum() {
2844        let json = r#"{"type":"object","properties":{"key":{"const":"only"}},"required":["key"]}"#;
2845        let schema: JsonSchema = serde_json::from_str(json).unwrap();
2846        let settings: CodeGenSettings = default_settings();
2847        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2848        let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
2849        let expected: &str = r#"//! Generated by json-schema-rs. Do not edit manually.
2850
2851use serde::{Deserialize, Serialize};
2852
2853#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2854pub enum Key {
2855    #[serde(rename = "only")]
2856    Only,
2857}
2858
2859#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2860pub struct Root {
2861    pub key: Key,
2862}
2863
2864"#;
2865        assert_eq!(expected, actual);
2866    }
2867
2868    #[test]
2869    fn all_of_merge_same_const_ok() {
2870        let s1: JsonSchema = serde_json::from_str(
2871            r#"{"type":"object","properties":{"x":{"type":"string","const":"same"}}}"#,
2872        )
2873        .unwrap();
2874        let s2: JsonSchema = serde_json::from_str(
2875            r#"{"type":"object","properties":{"x":{"type":"string","const":"same"}}}"#,
2876        )
2877        .unwrap();
2878        let actual: Result<JsonSchema, _> = merge_all_of(&[s1, s2]);
2879        assert!(actual.is_ok());
2880        let merged: JsonSchema = actual.unwrap();
2881        let x_schema: &JsonSchema = merged.properties.get("x").expect("property x");
2882        assert_eq!(
2883            x_schema.const_value.as_ref(),
2884            Some(&serde_json::Value::String("same".to_string()))
2885        );
2886    }
2887
2888    #[test]
2889    fn all_of_merge_conflicting_const_err() {
2890        let s1: JsonSchema = serde_json::from_str(
2891            r#"{"type":"object","properties":{"x":{"type":"string","const":"a"}}}"#,
2892        )
2893        .unwrap();
2894        let s2: JsonSchema = serde_json::from_str(
2895            r#"{"type":"object","properties":{"x":{"type":"string","const":"b"}}}"#,
2896        )
2897        .unwrap();
2898        let actual: Result<JsonSchema, CodeGenError> = merge_all_of(&[s1, s2]);
2899        let err = actual.expect_err("expected AllOfMergeConflictingConst");
2900        assert!(matches!(
2901            err,
2902            CodeGenError::AllOfMergeConflictingConst { .. }
2903        ));
2904    }
2905
2906    #[test]
2907    fn all_of_merge_same_pattern_ok() {
2908        let s1: JsonSchema = serde_json::from_str(
2909            r#"{"type":"object","properties":{"x":{"type":"string","pattern":"^[a-z]+$"}}}"#,
2910        )
2911        .unwrap();
2912        let s2: JsonSchema = serde_json::from_str(
2913            r#"{"type":"object","properties":{"x":{"type":"string","pattern":"^[a-z]+$"}}}"#,
2914        )
2915        .unwrap();
2916        let merged: JsonSchema = merge_all_of(&[s1, s2]).expect("merge ok");
2917        let actual: JsonSchema = merged.properties.get("x").cloned().expect("property x");
2918        let expected: JsonSchema = JsonSchema {
2919            type_: Some("string".to_string()),
2920            pattern: Some("^[a-z]+$".to_string()),
2921            ..Default::default()
2922        };
2923        assert_eq!(expected, actual);
2924    }
2925
2926    #[test]
2927    fn all_of_merge_conflicting_pattern_err() {
2928        let s1: JsonSchema = serde_json::from_str(
2929            r#"{"type":"object","properties":{"x":{"type":"string","pattern":"^[a-z]+$"}}}"#,
2930        )
2931        .unwrap();
2932        let s2: JsonSchema = serde_json::from_str(
2933            r#"{"type":"object","properties":{"x":{"type":"string","pattern":"^[0-9]+$"}}}"#,
2934        )
2935        .unwrap();
2936        let actual: Result<JsonSchema, CodeGenError> = merge_all_of(&[s1, s2]);
2937        assert!(matches!(
2938            actual,
2939            Err(CodeGenError::AllOfMergeConflictingPattern { .. })
2940        ));
2941    }
2942
2943    #[test]
2944    fn all_of_merged_object_golden() {
2945        let json = r#"{"allOf":[{"type":"object","properties":{"a":{"type":"string"}}},{"type":"object","properties":{"b":{"type":"integer"}},"required":["b"]}]}"#;
2946        let schema: JsonSchema = serde_json::from_str(json).unwrap();
2947        let settings = default_settings();
2948        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2949        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
2950        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
2951
2952use serde::{Deserialize, Serialize};
2953
2954#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2955pub struct Root {
2956    pub a: Option<String>,
2957    pub b: i64,
2958}
2959
2960";
2961        assert_eq!(expected, actual);
2962    }
2963
2964    #[test]
2965    fn anyof_property_golden() {
2966        let json = r#"{"type":"object","properties":{"foo":{"anyOf":[{"type":"string"},{"type":"object","properties":{"x":{"type":"integer"}}}]}}}"#;
2967        let schema: JsonSchema = serde_json::from_str(json).unwrap();
2968        let settings = default_settings();
2969        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
2970        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
2971        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
2972
2973use serde::{Deserialize, Serialize};
2974
2975#[derive(Debug, Clone, Serialize, Deserialize)]
2976pub enum FooAnyOf {
2977    Variant0(String),
2978    Variant1(FooVariant1),
2979}
2980
2981#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2982pub struct FooVariant1 {
2983    pub x: Option<i64>,
2984}
2985
2986#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
2987pub struct Root {
2988    pub foo: Option<FooAnyOf>,
2989}
2990
2991";
2992        assert_eq!(expected, actual);
2993    }
2994
2995    #[test]
2996    fn anyof_root_golden() {
2997        let json = r#"{"anyOf":[{"type":"object","properties":{"a":{"type":"string"}}},{"type":"object","properties":{"b":{"type":"integer"}}}]}"#;
2998        let schema: JsonSchema = serde_json::from_str(json).unwrap();
2999        let settings = default_settings();
3000        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3001        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3002        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3003
3004use serde::{Deserialize, Serialize};
3005
3006#[derive(Debug, Clone, Serialize, Deserialize)]
3007pub enum RootAnyOf {
3008    Variant0(RootVariant0),
3009    Variant1(RootVariant1),
3010}
3011
3012#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3013pub struct RootVariant0 {
3014    pub a: Option<String>,
3015}
3016
3017#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3018pub struct RootVariant1 {
3019    pub b: Option<i64>,
3020}
3021
3022";
3023        assert_eq!(expected, actual);
3024    }
3025
3026    #[test]
3027    fn anyof_empty_errors() {
3028        let schema: JsonSchema = JsonSchema {
3029            any_of: Some(vec![]),
3030            ..Default::default()
3031        };
3032        let settings = default_settings();
3033        let actual = generate_rust(&[schema], &settings).unwrap_err();
3034        assert!(matches!(actual, CodeGenError::Batch { index: 0, .. }));
3035        if let CodeGenError::Batch { source, .. } = actual {
3036            assert!(matches!(*source, CodeGenError::AnyOfEmpty));
3037        }
3038    }
3039
3040    #[test]
3041    fn oneof_property_golden() {
3042        let json = r#"{"type":"object","properties":{"foo":{"oneOf":[{"type":"string"},{"type":"object","properties":{"x":{"type":"integer"}}}]}}}"#;
3043        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3044        let settings = default_settings();
3045        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3046        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3047        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3048
3049use serde::{Deserialize, Serialize};
3050
3051#[derive(Debug, Clone, Serialize, Deserialize)]
3052pub enum FooOneOf {
3053    Variant0(String),
3054    Variant1(FooVariant1),
3055}
3056
3057#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3058pub struct FooVariant1 {
3059    pub x: Option<i64>,
3060}
3061
3062#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3063pub struct Root {
3064    pub foo: Option<FooOneOf>,
3065}
3066
3067";
3068        assert_eq!(expected, actual);
3069    }
3070
3071    #[test]
3072    fn oneof_root_golden() {
3073        let json = r#"{"oneOf":[{"type":"object","properties":{"a":{"type":"string"}}},{"type":"object","properties":{"b":{"type":"integer"}}}]}"#;
3074        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3075        let settings = default_settings();
3076        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3077        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3078        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3079
3080use serde::{Deserialize, Serialize};
3081
3082#[derive(Debug, Clone, Serialize, Deserialize)]
3083pub enum RootOneOf {
3084    Variant0(RootVariant0),
3085    Variant1(RootVariant1),
3086}
3087
3088#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3089pub struct RootVariant0 {
3090    pub a: Option<String>,
3091}
3092
3093#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3094pub struct RootVariant1 {
3095    pub b: Option<i64>,
3096}
3097
3098";
3099        assert_eq!(expected, actual);
3100    }
3101
3102    #[test]
3103    fn oneof_empty_errors() {
3104        let schema: JsonSchema = JsonSchema {
3105            one_of: Some(vec![]),
3106            ..Default::default()
3107        };
3108        let settings = default_settings();
3109        let actual = generate_rust(&[schema], &settings).unwrap_err();
3110        assert!(matches!(actual, CodeGenError::Batch { index: 0, .. }));
3111        if let CodeGenError::Batch { source, .. } = actual {
3112            assert!(matches!(*source, CodeGenError::OneOfEmpty));
3113        }
3114    }
3115
3116    #[test]
3117    fn merge_all_of_success_two_object_subschemas() {
3118        let s1: JsonSchema =
3119            serde_json::from_str(r#"{"type":"object","properties":{"a":{"type":"string"}}}"#)
3120                .unwrap();
3121        let s2: JsonSchema = serde_json::from_str(
3122            r#"{"type":"object","properties":{"b":{"type":"integer"}},"required":["b"]}"#,
3123        )
3124        .unwrap();
3125        let actual = merge_all_of(&[s1, s2]).unwrap();
3126        let expected: JsonSchema = serde_json::from_str(
3127            r#"{"type":"object","properties":{"a":{"type":"string"},"b":{"type":"integer"}},"required":["b"]}"#,
3128        )
3129        .unwrap();
3130        assert_eq!(expected, actual);
3131    }
3132
3133    #[test]
3134    fn merge_all_of_empty_errors() {
3135        let actual = merge_all_of(&[]);
3136        assert!(matches!(actual, Err(CodeGenError::AllOfMergeEmpty)));
3137    }
3138
3139    #[test]
3140    fn merge_all_of_single_schema() {
3141        let s: JsonSchema =
3142            serde_json::from_str(r#"{"type":"object","properties":{"x":{"type":"string"}}}"#)
3143                .unwrap();
3144        let expected = s.clone();
3145        let actual = merge_all_of(std::slice::from_ref(&s)).unwrap();
3146        assert_eq!(expected, actual);
3147    }
3148
3149    #[test]
3150    fn merge_all_of_conflicting_property_type_errors() {
3151        let s1: JsonSchema =
3152            serde_json::from_str(r#"{"type":"object","properties":{"k":{"type":"string"}}}"#)
3153                .unwrap();
3154        let s2: JsonSchema =
3155            serde_json::from_str(r#"{"type":"object","properties":{"k":{"type":"integer"}}}"#)
3156                .unwrap();
3157        let actual = merge_all_of(&[s1, s2]);
3158        assert!(matches!(
3159            actual,
3160            Err(CodeGenError::AllOfMergeConflictingPropertyType {
3161                property_key: ref k,
3162                ..
3163            }) if k == "k"
3164        ));
3165    }
3166
3167    #[test]
3168    fn merge_all_of_non_object_subschema_errors() {
3169        let s1: JsonSchema =
3170            serde_json::from_str(r#"{"type":"object","properties":{"a":{"type":"string"}}}"#)
3171                .unwrap();
3172        let s2: JsonSchema = serde_json::from_str(r#"{"type":"string"}"#).unwrap();
3173        let actual = merge_all_of(&[s1, s2]);
3174        assert!(matches!(
3175            actual,
3176            Err(CodeGenError::AllOfMergeNonObjectSubschema { index: 1 })
3177        ));
3178    }
3179
3180    #[test]
3181    fn merge_all_of_conflicting_enum_errors() {
3182        let s1: JsonSchema =
3183            serde_json::from_str(r#"{"type":"object","properties":{"s":{"enum":["a","b"]}}}"#)
3184                .unwrap();
3185        let s2: JsonSchema =
3186            serde_json::from_str(r#"{"type":"object","properties":{"s":{"enum":["x","y"]}}}"#)
3187                .unwrap();
3188        let actual = merge_all_of(&[s1, s2]);
3189        assert!(matches!(
3190            actual,
3191            Err(CodeGenError::AllOfMergeConflictingEnum { property_key: ref k }) if k == "s"
3192        ));
3193    }
3194
3195    #[test]
3196    fn merge_all_of_conflicting_numeric_bounds_errors() {
3197        let s1: JsonSchema = serde_json::from_str(
3198            r#"{"type":"object","properties":{"n":{"type":"integer","minimum":0,"maximum":10}}}"#,
3199        )
3200        .unwrap();
3201        let s2: JsonSchema = serde_json::from_str(
3202            r#"{"type":"object","properties":{"n":{"type":"integer","minimum":20,"maximum":30}}}"#,
3203        )
3204        .unwrap();
3205        let actual = merge_all_of(&[s1, s2]);
3206        assert!(matches!(
3207            actual,
3208            Err(CodeGenError::AllOfMergeConflictingNumericBounds {
3209                property_key: ref k,
3210                keyword: ref w
3211            }) if k == "n" && w == "minimum/maximum"
3212        ));
3213    }
3214
3215    #[test]
3216    fn batch_error_when_allof_merge_fails_in_second_schema() {
3217        let s0: JsonSchema =
3218            serde_json::from_str(r#"{"type":"object","properties":{"a":{"type":"string"}}}"#)
3219                .unwrap();
3220        let s1_bad: JsonSchema = serde_json::from_str(
3221            r#"{"allOf":[{"type":"object","properties":{"x":{"type":"string"}}},{"type":"object","properties":{"x":{"type":"integer"}}}]}"#,
3222        )
3223        .unwrap();
3224        let settings = default_settings();
3225        let actual = generate_rust(&[s0.clone(), s1_bad], &settings).unwrap_err();
3226        assert!(matches!(actual, CodeGenError::Batch { index: 1, .. }));
3227    }
3228
3229    #[test]
3230    fn root_all_of_merges_to_empty_object_errors_with_root_not_object() {
3231        let schema: JsonSchema =
3232            serde_json::from_str(r#"{"allOf":[{"type":"object"},{"type":"object"}]}"#).unwrap();
3233        let settings = default_settings();
3234        let actual = generate_rust(&[schema], &settings).unwrap_err();
3235        assert!(
3236            matches!(actual, CodeGenError::Batch { index: 0, source: ref s } if matches!(**s, CodeGenError::RootNotObject)),
3237            "expected Batch {{ index: 0, source: RootNotObject }}, got {actual:?}"
3238        );
3239    }
3240
3241    #[test]
3242    fn optional_enum_property_emits_enum_and_struct() {
3243        let json = r#"{"type":"object","properties":{"level":{"enum":["low","medium","high"]}}}"#;
3244        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3245        let settings: CodeGenSettings = default_settings();
3246        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3247        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3248        let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
3249
3250use serde::{Deserialize, Serialize};
3251
3252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3253pub enum Level {
3254    #[serde(rename = "high")]
3255    High,
3256    #[serde(rename = "low")]
3257    Low,
3258    #[serde(rename = "medium")]
3259    Medium,
3260}
3261
3262#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3263pub struct Root {
3264    pub level: Option<Level>,
3265}
3266
3267"#;
3268        assert_eq!(expected, actual);
3269    }
3270
3271    #[test]
3272    fn enum_dedupe_two_properties_same_enum_emits_one_enum() {
3273        let json = r#"{"type":"object","properties":{"a":{"enum":["x","y"]},"b":{"enum":["x","y"]}},"required":["a"]}"#;
3274        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3275        let settings: CodeGenSettings = default_settings();
3276        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3277        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3278        let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
3279
3280use serde::{Deserialize, Serialize};
3281
3282#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3283pub enum A {
3284    #[serde(rename = "x")]
3285    X,
3286    #[serde(rename = "y")]
3287    Y,
3288}
3289
3290#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3291pub struct Root {
3292    pub a: A,
3293    pub b: Option<A>,
3294}
3295
3296"#;
3297        assert_eq!(expected, actual);
3298    }
3299
3300    #[test]
3301    fn enum_collision_emits_suffixed_variants() {
3302        let json = r#"{"type":"object","properties":{"t":{"enum":["a","A"]}},"required":["t"]}"#;
3303        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3304        let settings: CodeGenSettings = default_settings();
3305        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3306        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3307        let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
3308
3309use serde::{Deserialize, Serialize};
3310
3311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3312pub enum T {
3313    #[serde(rename = "A")]
3314    A0,
3315    #[serde(rename = "a")]
3316    A1,
3317}
3318
3319#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3320pub struct Root {
3321    pub t: T,
3322}
3323
3324"#;
3325        assert_eq!(expected, actual);
3326    }
3327
3328    #[test]
3329    fn enum_duplicate_values_in_schema_emits_single_variant_per_value() {
3330        let json = r#"{"type":"object","properties":{"t":{"enum":["A","A","A","a","a","a","a","a","a","a"]}},"required":["t"]}"#;
3331        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3332        let settings: CodeGenSettings = default_settings();
3333        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3334        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3335        let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
3336
3337use serde::{Deserialize, Serialize};
3338
3339#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3340pub enum T {
3341    #[serde(rename = "A")]
3342    A0,
3343    #[serde(rename = "a")]
3344    A1,
3345}
3346
3347#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3348pub struct Root {
3349    pub t: T,
3350}
3351
3352"#;
3353        assert_eq!(expected, actual);
3354    }
3355
3356    #[test]
3357    fn enum_non_rust_compliant_values_emits_valid_variants() {
3358        let json = r#"{"type":"object","properties":{"id":{"enum":["/8633","todd.griffin"]}},"required":["id"]}"#;
3359        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3360        let settings: CodeGenSettings = default_settings();
3361        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3362        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3363        let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
3364
3365use serde::{Deserialize, Serialize};
3366
3367#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3368pub enum Id {
3369    #[serde(rename = "/8633")]
3370    E8633,
3371    #[serde(rename = "todd.griffin")]
3372    ToddGriffin,
3373}
3374
3375#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3376pub struct Root {
3377    pub id: Id,
3378}
3379
3380"#;
3381        assert_eq!(expected, actual);
3382    }
3383
3384    #[test]
3385    fn camel_case_property_emits_snake_case_field() {
3386        let json = r#"{"type":"object","properties":{"toddGriffin":{"type":"string"}},"required":["toddGriffin"]}"#;
3387        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3388        let settings: CodeGenSettings = default_settings();
3389        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3390        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3391        let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
3392
3393use serde::{Deserialize, Serialize};
3394
3395#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3396pub struct Root {
3397    #[serde(rename = "toddGriffin")]
3398    pub todd_griffin: String,
3399}
3400
3401"#;
3402        assert_eq!(expected, actual);
3403    }
3404
3405    #[test]
3406    fn default_function_name_uses_snake_case() {
3407        let json = r#"{"type":"object","properties":{"person":{"title":"ValerieHunter","type":"object","properties":{"toddGriffin":{"type":"string","default":"bar"}},"required":["toddGriffin"]}},"required":["person"]}"#;
3408        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3409        let settings: CodeGenSettings = default_settings();
3410        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3411        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3412        let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
3413
3414use serde::{Deserialize, Serialize};
3415
3416fn default_valerie_hunter_todd_griffin() -> String { "bar".to_string() }
3417
3418#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3419#[json_schema(title = "ValerieHunter")]
3420pub struct ValerieHunter {
3421    #[serde(rename = "toddGriffin")]
3422    #[serde(default = "default_valerie_hunter_todd_griffin")]
3423    pub todd_griffin: String,
3424}
3425
3426#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3427pub struct Root {
3428    pub person: ValerieHunter,
3429}
3430
3431"#;
3432        assert_eq!(expected, actual);
3433    }
3434
3435    #[test]
3436    fn non_string_enum_fallback_emits_string() {
3437        let json =
3438            r#"{"type":"object","properties":{"tag":{"enum":["foo",1,true]}},"required":["tag"]}"#;
3439        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3440        let settings: CodeGenSettings = default_settings();
3441        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3442        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3443        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3444
3445use serde::{Deserialize, Serialize};
3446
3447#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3448pub struct Root {
3449    pub tag: String,
3450}
3451
3452";
3453        assert_eq!(expected, actual);
3454    }
3455
3456    #[test]
3457    fn single_required_integer_property() {
3458        let json =
3459            r#"{"type":"object","properties":{"count":{"type":"integer"}},"required":["count"]}"#;
3460        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3461        let settings: CodeGenSettings = default_settings();
3462        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3463        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3464        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3465
3466use serde::{Deserialize, Serialize};
3467
3468#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3469pub struct Root {
3470    pub count: i64,
3471}
3472
3473";
3474        assert_eq!(expected, actual);
3475    }
3476
3477    #[test]
3478    fn single_optional_integer_property() {
3479        let json = r#"{"type":"object","properties":{"count":{"type":"integer"}}}"#;
3480        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3481        let settings: CodeGenSettings = default_settings();
3482        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3483        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3484        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3485
3486use serde::{Deserialize, Serialize};
3487
3488#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3489pub struct Root {
3490    pub count: Option<i64>,
3491}
3492
3493";
3494        assert_eq!(expected, actual);
3495    }
3496
3497    #[test]
3498    fn single_required_float_property() {
3499        let json =
3500            r#"{"type":"object","properties":{"value":{"type":"number"}},"required":["value"]}"#;
3501        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3502        let settings: CodeGenSettings = default_settings();
3503        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3504        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3505        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3506
3507use serde::{Deserialize, Serialize};
3508
3509#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3510pub struct Root {
3511    pub value: f64,
3512}
3513
3514";
3515        assert_eq!(expected, actual);
3516    }
3517
3518    #[test]
3519    fn single_optional_float_property() {
3520        let json = r#"{"type":"object","properties":{"value":{"type":"number"}}}"#;
3521        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3522        let settings: CodeGenSettings = default_settings();
3523        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3524        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3525        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3526
3527use serde::{Deserialize, Serialize};
3528
3529#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3530pub struct Root {
3531    pub value: Option<f64>,
3532}
3533
3534";
3535        assert_eq!(expected, actual);
3536    }
3537
3538    #[test]
3539    fn integer_with_minimum_maximum_u8_range_emits_u8() {
3540        let json = r#"{"type":"object","properties":{"byte":{"type":"integer","minimum":0,"maximum":255}},"required":["byte"]}"#;
3541        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3542        let settings: CodeGenSettings = default_settings();
3543        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3544        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3545        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3546
3547use serde::{Deserialize, Serialize};
3548
3549#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3550pub struct Root {
3551    pub byte: u8,
3552}
3553
3554";
3555        assert_eq!(expected, actual);
3556    }
3557
3558    #[test]
3559    fn integer_with_only_minimum_emits_i64_fallback() {
3560        let json = r#"{"type":"object","properties":{"count":{"type":"integer","minimum":0}}}"#;
3561        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3562        let settings: CodeGenSettings = default_settings();
3563        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3564        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3565        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3566
3567use serde::{Deserialize, Serialize};
3568
3569#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3570pub struct Root {
3571    pub count: Option<i64>,
3572}
3573
3574";
3575        assert_eq!(expected, actual);
3576    }
3577
3578    #[test]
3579    fn integer_with_only_maximum_emits_i64_fallback() {
3580        let json = r#"{"type":"object","properties":{"count":{"type":"integer","maximum":100}}}"#;
3581        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3582        let settings: CodeGenSettings = default_settings();
3583        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3584        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3585        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3586
3587use serde::{Deserialize, Serialize};
3588
3589#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3590pub struct Root {
3591    pub count: Option<i64>,
3592}
3593
3594";
3595        assert_eq!(expected, actual);
3596    }
3597
3598    #[test]
3599    fn number_without_min_max_emits_f64_fallback() {
3600        let json =
3601            r#"{"type":"object","properties":{"value":{"type":"number"}},"required":["value"]}"#;
3602        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3603        let settings: CodeGenSettings = default_settings();
3604        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3605        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3606        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3607
3608use serde::{Deserialize, Serialize};
3609
3610#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3611pub struct Root {
3612    pub value: f64,
3613}
3614
3615";
3616        assert_eq!(expected, actual);
3617    }
3618
3619    #[test]
3620    fn number_with_minimum_maximum_f32_range_emits_f32() {
3621        let json = r#"{"type":"object","properties":{"value":{"type":"number","minimum":0.5,"maximum":100.5}},"required":["value"]}"#;
3622        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3623        let settings: CodeGenSettings = default_settings();
3624        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3625        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3626        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3627
3628use serde::{Deserialize, Serialize};
3629
3630#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3631pub struct Root {
3632    pub value: f32,
3633}
3634
3635";
3636        assert_eq!(expected, actual);
3637    }
3638
3639    #[test]
3640    fn boolean_property_required_emits_bool() {
3641        let json = r#"{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"]}"#;
3642        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3643        let settings: CodeGenSettings = default_settings();
3644        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3645        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3646        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3647
3648use serde::{Deserialize, Serialize};
3649
3650#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3651pub struct Root {
3652    pub enabled: bool,
3653}
3654
3655";
3656        assert_eq!(expected, actual);
3657    }
3658
3659    #[test]
3660    fn boolean_property_optional_emits_option_bool() {
3661        let json = r#"{"type":"object","properties":{"flag":{"type":"boolean"}}}"#;
3662        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3663        let settings: CodeGenSettings = default_settings();
3664        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3665        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3666        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3667
3668use serde::{Deserialize, Serialize};
3669
3670#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3671pub struct Root {
3672    pub flag: Option<bool>,
3673}
3674
3675";
3676        assert_eq!(expected, actual);
3677    }
3678
3679    #[test]
3680    fn boolean_property_with_description_emits_doc_and_field() {
3681        let json = r#"{"type":"object","properties":{"flag":{"type":"boolean","description":"Is it enabled?"}}}"#;
3682        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3683        let settings: CodeGenSettings = default_settings();
3684        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3685        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3686        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3687
3688use serde::{Deserialize, Serialize};
3689
3690#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3691pub struct Root {
3692    /// Is it enabled?
3693    pub flag: Option<bool>,
3694}
3695
3696";
3697        assert_eq!(expected, actual);
3698    }
3699
3700    #[test]
3701    fn array_items_boolean_emits_vec_bool() {
3702        let json = r#"{"type":"object","properties":{"flags":{"type":"array","items":{"type":"boolean"}}},"required":["flags"]}"#;
3703        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3704        let settings: CodeGenSettings = default_settings();
3705        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3706        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3707        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3708
3709use serde::{Deserialize, Serialize};
3710
3711#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3712pub struct Root {
3713    pub flags: Vec<bool>,
3714}
3715
3716";
3717        assert_eq!(expected, actual);
3718    }
3719
3720    #[test]
3721    fn array_items_boolean_unique_items_emits_hash_set_bool() {
3722        let json = r#"{"type":"object","properties":{"flags":{"type":"array","items":{"type":"boolean"},"uniqueItems":true}},"required":["flags"]}"#;
3723        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3724        let settings: CodeGenSettings = default_settings();
3725        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3726        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3727        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3728
3729use serde::{Deserialize, Serialize};
3730use std::collections::HashSet;
3731
3732#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3733pub struct Root {
3734    pub flags: HashSet<bool>,
3735}
3736
3737";
3738        assert_eq!(expected, actual);
3739    }
3740
3741    #[test]
3742    fn boolean_property_with_default_emits_default_attr() {
3743        let json =
3744            r#"{"type":"object","properties":{"enabled":{"type":"boolean","default":true}}}"#;
3745        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3746        let settings: CodeGenSettings = default_settings();
3747        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3748        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3749        let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
3750
3751use serde::{Deserialize, Serialize};
3752
3753fn default_root_enabled() -> Option<bool> { Some(true) }
3754
3755#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3756pub struct Root {
3757    #[serde(default = "default_root_enabled")]
3758    pub enabled: Option<bool>,
3759}
3760
3761"#;
3762        assert_eq!(expected, actual);
3763    }
3764
3765    #[test]
3766    fn array_required_string_property() {
3767        let json = r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"}}},"required":["tags"]}"#;
3768        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3769        let settings: CodeGenSettings = default_settings();
3770        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3771        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3772        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3773
3774use serde::{Deserialize, Serialize};
3775
3776#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3777pub struct Root {
3778    pub tags: Vec<String>,
3779}
3780
3781";
3782        assert_eq!(expected, actual);
3783    }
3784
3785    #[test]
3786    fn array_with_unique_items_true_emits_hash_set_string() {
3787        let json = r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"},"uniqueItems":true}},"required":["tags"]}"#;
3788        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3789        let settings: CodeGenSettings = default_settings();
3790        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3791        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3792        assert!(
3793            actual.contains("pub tags: HashSet<String>"),
3794            "expected HashSet<String>: {actual}"
3795        );
3796        assert!(
3797            actual.contains(concat!("use std::collections::", "HashSet")),
3798            "expected HashSet use: {actual}"
3799        );
3800    }
3801
3802    #[test]
3803    fn array_with_unique_items_false_emits_vec_string() {
3804        let json = r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"},"uniqueItems":false}},"required":["tags"]}"#;
3805        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3806        let settings: CodeGenSettings = default_settings();
3807        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3808        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3809        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3810
3811use serde::{Deserialize, Serialize};
3812
3813#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3814pub struct Root {
3815    pub tags: Vec<String>,
3816}
3817
3818";
3819        assert_eq!(expected, actual);
3820    }
3821
3822    #[test]
3823    fn array_with_unique_items_true_object_items_emits_vec() {
3824        let json = r#"{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"}}},"uniqueItems":true}},"required":["items"]}"#;
3825        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3826        let settings: CodeGenSettings = default_settings();
3827        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3828        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3829        assert!(
3830            actual.contains("pub items: Vec<") && actual.contains(">,"),
3831            "uniqueItems true with object items should emit Vec: {actual}"
3832        );
3833        assert!(
3834            !actual.contains("HashSet"),
3835            "should not use HashSet for object items: {actual}"
3836        );
3837    }
3838
3839    #[test]
3840    fn array_with_min_items_max_items_emits_attribute() {
3841        let json = r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":5}},"required":["tags"]}"#;
3842        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3843        let settings: CodeGenSettings = default_settings();
3844        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3845        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3846        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3847
3848use serde::{Deserialize, Serialize};
3849
3850#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3851pub struct Root {
3852    #[json_schema(min_items = 2, max_items = 5)]
3853    pub tags: Vec<String>,
3854}
3855
3856";
3857        assert_eq!(expected, actual);
3858    }
3859
3860    #[test]
3861    fn array_with_min_items_only_emits_attribute() {
3862        let json = r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"},"minItems":1}},"required":["tags"]}"#;
3863        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3864        let settings: CodeGenSettings = default_settings();
3865        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3866        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3867        assert!(
3868            actual.contains("#[json_schema(min_items = 1)]"),
3869            "expected min_items attribute: {actual}"
3870        );
3871        assert!(
3872            !actual.contains("max_items"),
3873            "should not emit max_items when absent: {actual}"
3874        );
3875    }
3876
3877    #[test]
3878    fn array_optional_string_property() {
3879        let json =
3880            r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"}}}}"#;
3881        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3882        let settings: CodeGenSettings = default_settings();
3883        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3884        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3885        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3886
3887use serde::{Deserialize, Serialize};
3888
3889#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3890pub struct Root {
3891    pub tags: Option<Vec<String>>,
3892}
3893
3894";
3895        assert_eq!(expected, actual);
3896    }
3897
3898    #[test]
3899    fn array_of_integers_property() {
3900        let json = r#"{"type":"object","properties":{"counts":{"type":"array","items":{"type":"integer"}}},"required":["counts"]}"#;
3901        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3902        let settings: CodeGenSettings = default_settings();
3903        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3904        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3905        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3906
3907use serde::{Deserialize, Serialize};
3908
3909#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3910pub struct Root {
3911    pub counts: Vec<i64>,
3912}
3913
3914";
3915        assert_eq!(expected, actual);
3916    }
3917
3918    #[test]
3919    fn array_of_objects_property() {
3920        let json = r#"{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"}}}}},"required":["items"]}"#;
3921        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3922        let settings: CodeGenSettings = default_settings();
3923        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3924        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3925        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3926
3927use serde::{Deserialize, Serialize};
3928
3929#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3930pub struct Items {
3931    pub name: Option<String>,
3932}
3933
3934#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3935pub struct Root {
3936    pub items: Vec<Items>,
3937}
3938
3939";
3940        assert_eq!(expected, actual);
3941    }
3942
3943    #[test]
3944    fn array_of_arrays_property() {
3945        let json = r#"{"type":"object","properties":{"matrix":{"type":"array","items":{"type":"array","items":{"type":"string"}}}},"required":["matrix"]}"#;
3946        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3947        let settings: CodeGenSettings = default_settings();
3948        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3949        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3950        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3951
3952use serde::{Deserialize, Serialize};
3953
3954#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3955pub struct Root {
3956    pub matrix: Vec<Vec<String>>,
3957}
3958
3959";
3960        assert_eq!(expected, actual);
3961    }
3962
3963    #[test]
3964    fn mixed_string_integer_float_properties() {
3965        let json = r#"{"type":"object","properties":{"id":{"type":"string"},"count":{"type":"integer"},"value":{"type":"number"}},"required":["id"]}"#;
3966        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3967        let settings: CodeGenSettings = default_settings();
3968        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3969        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3970        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3971
3972use serde::{Deserialize, Serialize};
3973
3974#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3975pub struct Root {
3976    pub count: Option<i64>,
3977    pub id: String,
3978    pub value: Option<f64>,
3979}
3980
3981";
3982        assert_eq!(expected, actual);
3983    }
3984
3985    #[test]
3986    fn mixed_string_and_integer_properties() {
3987        let json = r#"{"type":"object","properties":{"id":{"type":"string"},"count":{"type":"integer"}},"required":["id"]}"#;
3988        let schema: JsonSchema = serde_json::from_str(json).unwrap();
3989        let settings: CodeGenSettings = default_settings();
3990        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
3991        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
3992        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
3993
3994use serde::{Deserialize, Serialize};
3995
3996#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
3997pub struct Root {
3998    pub count: Option<i64>,
3999    pub id: String,
4000}
4001
4002";
4003        assert_eq!(expected, actual);
4004    }
4005
4006    #[test]
4007    fn nested_object_and_rename() {
4008        let json = r#"{
4009          "type": "object",
4010          "properties": {
4011            "first_name": { "type": "string" },
4012            "address": {
4013              "type": "object",
4014              "properties": {
4015                "street_address": { "type": "string" },
4016                "city": { "type": "string" }
4017              }
4018            }
4019          }
4020        }"#;
4021        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4022        let settings: CodeGenSettings = default_settings();
4023        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4024        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4025        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
4026
4027use serde::{Deserialize, Serialize};
4028
4029#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4030pub struct Address {
4031    pub city: Option<String>,
4032    pub street_address: Option<String>,
4033}
4034
4035#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4036pub struct Root {
4037    pub address: Option<Address>,
4038    pub first_name: Option<String>,
4039}
4040
4041";
4042        assert_eq!(expected, actual);
4043    }
4044
4045    #[test]
4046    fn full_example_from_plan() {
4047        let json = r#"{
4048          "type": "object",
4049          "properties": {
4050            "first_name": { "type": "string" },
4051            "last_name": { "type": "string" },
4052            "birthday": { "type": "string" },
4053            "address": {
4054              "type": "object",
4055              "properties": {
4056                "street_address": { "type": "string" },
4057                "city": { "type": "string" },
4058                "state": { "type": "string" },
4059                "country": { "type": "string" }
4060              }
4061            }
4062          }
4063        }"#;
4064        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4065        let settings: CodeGenSettings = default_settings();
4066        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4067        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4068        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
4069
4070use serde::{Deserialize, Serialize};
4071
4072#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4073pub struct Address {
4074    pub city: Option<String>,
4075    pub country: Option<String>,
4076    pub state: Option<String>,
4077    pub street_address: Option<String>,
4078}
4079
4080#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4081pub struct Root {
4082    pub address: Option<Address>,
4083    pub birthday: Option<String>,
4084    pub first_name: Option<String>,
4085    pub last_name: Option<String>,
4086}
4087
4088";
4089        assert_eq!(expected, actual);
4090    }
4091
4092    #[test]
4093    fn deeply_nested_schema_does_not_stack_overflow() {
4094        const DEPTH: usize = 150;
4095        let mut inner: JsonSchema = JsonSchema {
4096            type_: Some("object".to_string()),
4097            properties: {
4098                let mut m = std::collections::BTreeMap::new();
4099                m.insert(
4100                    "value".to_string(),
4101                    JsonSchema {
4102                        type_: Some("string".to_string()),
4103                        ..Default::default()
4104                    },
4105                );
4106                m
4107            },
4108            title: Some("Leaf".to_string()),
4109            ..Default::default()
4110        };
4111        for i in (0..DEPTH).rev() {
4112            let mut wrap: JsonSchema = JsonSchema {
4113                type_: Some("object".to_string()),
4114                title: Some(format!("Level{i}")),
4115                ..Default::default()
4116            };
4117            wrap.properties.insert("child".to_string(), inner);
4118            inner = wrap;
4119        }
4120        let settings: CodeGenSettings = default_settings();
4121        let actual = generate_rust(&[inner], &settings);
4122        assert!(actual.is_ok(), concat!("deep schema must not ", "overflow"));
4123        let out = actual.unwrap();
4124        let output: String = String::from_utf8(out.per_schema[0].clone()).unwrap();
4125        assert!(
4126            output.contains(concat!("pub struct ", "Level0")),
4127            concat!("output must contain root ", "struct")
4128        );
4129        assert!(
4130            output.contains(concat!("pub struct ", "Leaf")),
4131            concat!("output must contain leaf ", "struct")
4132        );
4133    }
4134
4135    #[test]
4136    fn field_rename_when_key_differs_from_identifier() {
4137        let json = r#"{"type":"object","properties":{"foo-bar":{"type":"string"}}}"#;
4138        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4139        let settings: CodeGenSettings = default_settings();
4140        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4141        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4142        let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
4143
4144use serde::{Deserialize, Serialize};
4145
4146#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4147pub struct Root {
4148    #[serde(rename = "foo-bar")]
4149    pub foo_bar: Option<String>,
4150}
4151
4152"#;
4153        assert_eq!(expected, actual);
4154    }
4155
4156    #[test]
4157    fn generate_rust_one_schema_returns_one_buffer() {
4158        let json = r#"{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]}"#;
4159        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4160        let settings: CodeGenSettings = default_settings();
4161        let output: super::GenerateRustOutput =
4162            generate_rust(std::slice::from_ref(&schema), &settings).unwrap();
4163        let expected: super::GenerateRustOutput =
4164            RustBackend.generate(&[schema], &settings).unwrap();
4165        assert_eq!(expected.per_schema, output.per_schema);
4166        assert_eq!(1, output.per_schema.len());
4167    }
4168
4169    #[test]
4170    fn property_key_first_uses_key_over_title_for_nested_struct() {
4171        let json = r#"{"type":"object","properties":{"address":{"type":"object","title":"FooBar","properties":{"city":{"type":"string"}}}}}"#;
4172        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4173        let settings: CodeGenSettings = CodeGenSettings::builder()
4174            .model_name_source(ModelNameSource::PropertyKeyFirst)
4175            .build();
4176        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4177        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4178        assert!(
4179            actual.contains(concat!("pub struct ", "Address")),
4180            "with PropertyKeyFirst nested struct should be named from key address -> Address; got: {actual}"
4181        );
4182        assert!(
4183            !actual.contains(concat!("struct ", "FooBar")),
4184            "with PropertyKeyFirst title FooBar should not be used for nested name; got: {actual}"
4185        );
4186    }
4187
4188    #[test]
4189    fn generate_rust_two_schemas_returns_two_buffers() {
4190        let json1 = r#"{"type":"object","properties":{"a":{"type":"string"}}}"#;
4191        let json2 = r#"{"type":"object","properties":{"b":{"type":"string"}}}"#;
4192        let s1: JsonSchema = serde_json::from_str(json1).unwrap();
4193        let s2: JsonSchema = serde_json::from_str(json2).unwrap();
4194        let settings: CodeGenSettings = default_settings();
4195        let output: super::GenerateRustOutput =
4196            generate_rust(&[s1.clone(), s2.clone()], &settings).unwrap();
4197        let expected: super::GenerateRustOutput =
4198            RustBackend.generate(&[s1, s2], &settings).unwrap();
4199        assert_eq!(expected.per_schema, output.per_schema);
4200        assert_eq!(2, output.per_schema.len());
4201        let out1 = String::from_utf8(output.per_schema[0].clone()).unwrap();
4202        let out2 = String::from_utf8(output.per_schema[1].clone()).unwrap();
4203        assert!(out1.contains("pub a: Option<String>") || out1.contains("pub a:"));
4204        assert!(out2.contains("pub b: Option<String>") || out2.contains("pub b:"));
4205    }
4206
4207    #[test]
4208    fn batch_error_includes_index() {
4209        let valid = r#"{"type":"object","properties":{"x":{"type":"string"}}}"#;
4210        let invalid: JsonSchema = JsonSchema::default();
4211        let s1: JsonSchema = serde_json::from_str(valid).unwrap();
4212        let settings: CodeGenSettings = default_settings();
4213        let actual = generate_rust(&[s1, invalid], &settings).unwrap_err();
4214        assert!(matches!(actual, CodeGenError::Batch { index: 1, .. }));
4215    }
4216
4217    #[test]
4218    fn dedupe_disabled_returns_no_shared() {
4219        let json = r#"{"type":"object","properties":{"a":{"type":"string"}}}"#;
4220        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4221        let settings: CodeGenSettings = CodeGenSettings::builder()
4222            .dedupe_mode(DedupeMode::Disabled)
4223            .build();
4224        let output: super::GenerateRustOutput =
4225            generate_rust(&[schema.clone(), schema], &settings).unwrap();
4226        let expected: Option<Vec<u8>> = None;
4227        let actual = output.shared.clone();
4228        assert_eq!(expected, actual);
4229        assert_eq!(2, output.per_schema.len());
4230    }
4231
4232    #[test]
4233    fn dedupe_disabled_two_schemas_same_shape_two_buffers_no_shared() {
4234        let json = r#"{"type":"object","properties":{"id":{"type":"string"}}}"#;
4235        let s1: JsonSchema = serde_json::from_str(json).unwrap();
4236        let s2: JsonSchema = serde_json::from_str(json).unwrap();
4237        let settings: CodeGenSettings = CodeGenSettings::builder()
4238            .dedupe_mode(DedupeMode::Disabled)
4239            .build();
4240        let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4241        assert_eq!(None, output.shared);
4242        assert_eq!(2, output.per_schema.len());
4243        let out0 = String::from_utf8(output.per_schema[0].clone()).unwrap();
4244        let out1 = String::from_utf8(output.per_schema[1].clone()).unwrap();
4245        let root_struct: &str = concat!("pub struct ", "Root");
4246        assert!(out0.contains(root_struct));
4247        assert!(out1.contains(root_struct));
4248    }
4249
4250    #[test]
4251    fn dedupe_full_two_identical_schemas_produces_shared() {
4252        let json = r#"{"type":"object","properties":{"id":{"type":"string"}}}"#;
4253        let s1: JsonSchema = serde_json::from_str(json).unwrap();
4254        let s2: JsonSchema = serde_json::from_str(json).unwrap();
4255        let settings: CodeGenSettings = CodeGenSettings::builder()
4256            .dedupe_mode(DedupeMode::Full)
4257            .build();
4258        let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4259        let expected_shared_some = true;
4260        let actual_shared_some = output.shared.is_some();
4261        assert_eq!(expected_shared_some, actual_shared_some);
4262        assert_eq!(2, output.per_schema.len());
4263        let shared_str = String::from_utf8(output.shared.unwrap()).unwrap();
4264        let root_struct: &str = concat!("pub struct ", "Root");
4265        assert!(shared_str.contains(root_struct));
4266        let per0 = String::from_utf8(output.per_schema[0].clone()).unwrap();
4267        let root_use: &str = concat!("pub use crate::", "Root");
4268        let root_only: &str = "Root";
4269        assert!(per0.contains(root_use) || per0.contains(root_only));
4270    }
4271
4272    #[test]
4273    fn dedupe_full_single_schema_no_shared() {
4274        let json = r#"{"type":"object","properties":{"a":{"type":"string"}}}"#;
4275        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4276        let settings: CodeGenSettings = CodeGenSettings::builder()
4277            .dedupe_mode(DedupeMode::Full)
4278            .build();
4279        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4280        let expected: Option<Vec<u8>> = None;
4281        let actual = output.shared.clone();
4282        assert_eq!(expected, actual);
4283        assert_eq!(1, output.per_schema.len());
4284    }
4285
4286    #[test]
4287    fn dedupe_full_two_different_schemas_no_shared() {
4288        let j1 = r#"{"type":"object","properties":{"a":{"type":"string"}}}"#;
4289        let j2 = r#"{"type":"object","properties":{"b":{"type":"string"}}}"#;
4290        let s1: JsonSchema = serde_json::from_str(j1).unwrap();
4291        let s2: JsonSchema = serde_json::from_str(j2).unwrap();
4292        let settings: CodeGenSettings = CodeGenSettings::builder()
4293            .dedupe_mode(DedupeMode::Full)
4294            .build();
4295        let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4296        assert_eq!(None, output.shared);
4297        assert_eq!(2, output.per_schema.len());
4298    }
4299
4300    #[test]
4301    fn dedupe_full_single_schema_two_identical_nested_objects_deduped() {
4302        let json = r#"{
4303            "type": "object",
4304            "properties": {
4305                "addr1": { "type": "object", "properties": { "street": { "type": "string" } } },
4306                "addr2": { "type": "object", "properties": { "street": { "type": "string" } } }
4307            }
4308        }"#;
4309        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4310        let settings: CodeGenSettings = CodeGenSettings::builder()
4311            .dedupe_mode(DedupeMode::Full)
4312            .build();
4313        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4314        let per_str = String::from_utf8(output.per_schema[0].clone()).unwrap();
4315        assert!(
4316            per_str.contains("addr1") && per_str.contains("addr2"),
4317            "per_schema should reference both fields; got: {per_str}"
4318        );
4319        let shared_count = output
4320            .shared
4321            .as_ref()
4322            .map_or(0, |b| b.windows(11).filter(|w| w == b"pub struct ").count());
4323        let per_count = per_str.matches("pub struct ").count();
4324        assert!(
4325            shared_count + per_count >= 1,
4326            "at least one struct (Root in per_schema, nested in shared when deduped)"
4327        );
4328    }
4329
4330    #[test]
4331    fn description_root_struct_single_line() {
4332        let json = r#"{"type":"object","properties":{"id":{"type":"string"}},"description":"A root type"}"#;
4333        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4334        let settings: CodeGenSettings = default_settings();
4335        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4336        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4337        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
4338
4339use serde::{Deserialize, Serialize};
4340
4341/// A root type
4342#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4343pub struct Root {
4344    pub id: Option<String>,
4345}
4346
4347";
4348        assert_eq!(expected, actual);
4349    }
4350
4351    #[test]
4352    fn description_root_struct_multi_line() {
4353        let json = r#"{"type":"object","properties":{"x":{"type":"string"}},"description":"Line one\nLine two"}"#;
4354        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4355        let settings: CodeGenSettings = default_settings();
4356        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4357        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4358        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
4359
4360use serde::{Deserialize, Serialize};
4361
4362/// Line one
4363/// Line two
4364#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4365pub struct Root {
4366    pub x: Option<String>,
4367}
4368
4369";
4370        assert_eq!(expected, actual);
4371    }
4372
4373    #[test]
4374    fn description_empty_or_whitespace_emits_no_doc() {
4375        let json_empty =
4376            r#"{"type":"object","properties":{"a":{"type":"string"}},"description":""}"#;
4377        let schema_empty: JsonSchema = serde_json::from_str(json_empty).unwrap();
4378        let settings: CodeGenSettings = default_settings();
4379        let output: super::GenerateRustOutput = generate_rust(&[schema_empty], &settings).unwrap();
4380        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4381        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
4382
4383use serde::{Deserialize, Serialize};
4384
4385#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4386pub struct Root {
4387    pub a: Option<String>,
4388}
4389
4390";
4391        assert_eq!(expected, actual);
4392
4393        let json_ws =
4394            r#"{"type":"object","properties":{"a":{"type":"string"}},"description":"   \n  "}"#;
4395        let schema_ws: JsonSchema = serde_json::from_str(json_ws).unwrap();
4396        let output_ws: super::GenerateRustOutput = generate_rust(&[schema_ws], &settings).unwrap();
4397        let actual_ws = String::from_utf8(output_ws.per_schema[0].clone()).unwrap();
4398        assert_eq!(expected, actual_ws);
4399    }
4400
4401    #[test]
4402    fn description_nested_object_struct_doc() {
4403        let json = r#"{"type":"object","properties":{"nested":{"type":"object","description":"Inner type","properties":{"v":{"type":"string"}}}}}"#;
4404        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4405        let settings: CodeGenSettings = default_settings();
4406        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4407        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4408        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
4409
4410use serde::{Deserialize, Serialize};
4411
4412/// Inner type
4413#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4414pub struct Nested {
4415    pub v: Option<String>,
4416}
4417
4418#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4419pub struct Root {
4420    /// Inner type
4421    pub nested: Option<Nested>,
4422}
4423
4424";
4425        assert_eq!(expected, actual);
4426    }
4427
4428    #[test]
4429    fn description_property_field_doc() {
4430        let json = r#"{"type":"object","properties":{"name":{"type":"string","description":"User full name"}}}"#;
4431        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4432        let settings: CodeGenSettings = default_settings();
4433        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4434        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4435        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
4436
4437use serde::{Deserialize, Serialize};
4438
4439#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4440pub struct Root {
4441    /// User full name
4442    pub name: Option<String>,
4443}
4444
4445";
4446        assert_eq!(expected, actual);
4447    }
4448
4449    #[test]
4450    fn description_enum_property_emits_enum_doc() {
4451        let json = r#"{"type":"object","properties":{"status":{"enum":["open","closed"],"description":"Issue status"}},"required":["status"]}"#;
4452        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4453        let settings: CodeGenSettings = default_settings();
4454        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4455        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4456        let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
4457
4458use serde::{Deserialize, Serialize};
4459
4460/// Issue status
4461#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4462pub enum Status {
4463    #[serde(rename = "closed")]
4464    Closed,
4465    #[serde(rename = "open")]
4466    Open,
4467}
4468
4469#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4470pub struct Root {
4471    /// Issue status
4472    pub status: Status,
4473}
4474
4475"#;
4476        assert_eq!(expected, actual);
4477    }
4478
4479    #[test]
4480    fn dedupe_functional_same_shape_different_description_one_struct() {
4481        let j1 = r#"{"type":"object","properties":{"id":{"type":"string"}},"description":"First"}"#;
4482        let j2 =
4483            r#"{"type":"object","properties":{"id":{"type":"string"}},"description":"Second"}"#;
4484        let s1: JsonSchema = serde_json::from_str(j1).unwrap();
4485        let s2: JsonSchema = serde_json::from_str(j2).unwrap();
4486        let settings: CodeGenSettings = CodeGenSettings::builder()
4487            .dedupe_mode(DedupeMode::Functional)
4488            .build();
4489        let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4490        let expected_shared_some = true;
4491        let actual_shared_some = output.shared.is_some();
4492        assert_eq!(expected_shared_some, actual_shared_some);
4493    }
4494
4495    #[test]
4496    fn dedupe_full_same_shape_different_description_two_structs() {
4497        let j1 = r#"{"type":"object","properties":{"id":{"type":"string"}},"description":"First"}"#;
4498        let j2 =
4499            r#"{"type":"object","properties":{"id":{"type":"string"}},"description":"Second"}"#;
4500        let s1: JsonSchema = serde_json::from_str(j1).unwrap();
4501        let s2: JsonSchema = serde_json::from_str(j2).unwrap();
4502        let settings: CodeGenSettings = CodeGenSettings::builder()
4503            .dedupe_mode(DedupeMode::Full)
4504            .build();
4505        let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4506        let expected_shared_some = false;
4507        let actual_shared_some = output.shared.is_some();
4508        assert_eq!(expected_shared_some, actual_shared_some);
4509        assert_eq!(2, output.per_schema.len());
4510    }
4511
4512    #[test]
4513    fn dedupe_functional_same_shape_different_comment_one_struct() {
4514        let j1 = r#"{"type":"object","properties":{"id":{"type":"string"}},"$comment":"First"}"#;
4515        let j2 = r#"{"type":"object","properties":{"id":{"type":"string"}},"$comment":"Second"}"#;
4516        let s1: JsonSchema = serde_json::from_str(j1).unwrap();
4517        let s2: JsonSchema = serde_json::from_str(j2).unwrap();
4518        let settings: CodeGenSettings = CodeGenSettings::builder()
4519            .dedupe_mode(DedupeMode::Functional)
4520            .build();
4521        let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4522        let expected_shared_some = true;
4523        let actual_shared_some = output.shared.is_some();
4524        assert_eq!(expected_shared_some, actual_shared_some);
4525    }
4526
4527    #[test]
4528    fn dedupe_full_same_shape_different_comment_two_structs() {
4529        let j1 = r#"{"type":"object","properties":{"id":{"type":"string"}},"$comment":"First"}"#;
4530        let j2 = r#"{"type":"object","properties":{"id":{"type":"string"}},"$comment":"Second"}"#;
4531        let s1: JsonSchema = serde_json::from_str(j1).unwrap();
4532        let s2: JsonSchema = serde_json::from_str(j2).unwrap();
4533        let settings: CodeGenSettings = CodeGenSettings::builder()
4534            .dedupe_mode(DedupeMode::Full)
4535            .build();
4536        let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4537        let expected_shared_some = false;
4538        let actual_shared_some = output.shared.is_some();
4539        assert_eq!(expected_shared_some, actual_shared_some);
4540        assert_eq!(2, output.per_schema.len());
4541    }
4542
4543    #[test]
4544    fn examples_golden_with_examples_emits_doc() {
4545        let json: &str = r#"{"type":"object","properties":{"x":{"type":"string"}},"required":["x"],"examples":[{"x":"foo"}]}"#;
4546        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4547        let settings: CodeGenSettings = CodeGenSettings::default();
4548        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4549        let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
4550        let expected: String = "//! Generated by json-schema-rs. Do not edit manually.\n\nuse serde::{Deserialize, Serialize};\n\n/// \n/// # Examples\n/// \n/// {\"x\":\"foo\"}\n#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\npub struct Root {\n    pub x: String,\n}\n\n".to_string();
4551        assert_eq!(expected, actual);
4552    }
4553
4554    #[test]
4555    fn examples_struct_doc_contains_examples() {
4556        let json: &str = r#"{"type":"object","properties":{"x":{"type":"string"}},"required":["x"],"examples":["a",1]}"#;
4557        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4558        let settings: CodeGenSettings = CodeGenSettings::default();
4559        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4560        let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
4561        let expected: String = "//! Generated by json-schema-rs. Do not edit manually.\n\nuse serde::{Deserialize, Serialize};\n\n/// \n/// # Examples\n/// \n/// \"a\"\n/// 1\n#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\npub struct Root {\n    pub x: String,\n}\n\n".to_string();
4562        assert_eq!(expected, actual);
4563    }
4564
4565    #[test]
4566    fn examples_enum_doc_contains_examples() {
4567        let json: &str = r#"{"type":"object","properties":{"status":{"enum":["open"],"examples":["open"]}},"required":["status"]}"#;
4568        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4569        let settings: CodeGenSettings = CodeGenSettings::default();
4570        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4571        let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
4572        let expected: String = "//! Generated by json-schema-rs. Do not edit manually.\n\nuse serde::{Deserialize, Serialize};\n\n/// \n/// # Examples\n/// \n/// \"open\"\n#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\npub enum Status {\n    #[serde(rename = \"open\")]\n    Open,\n}\n\n#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\npub struct Root {\n    pub status: Status,\n}\n\n".to_string();
4573        assert_eq!(expected, actual);
4574    }
4575
4576    #[test]
4577    fn examples_empty_array_emits_no_doc() {
4578        let json: &str = r#"{"type":"object","properties":{"x":{"type":"string"}},"required":["x"],"examples":[]}"#;
4579        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4580        let settings: CodeGenSettings = CodeGenSettings::default();
4581        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4582        let actual: String = String::from_utf8(output.per_schema[0].clone()).unwrap();
4583        let expected: String =
4584            "//! Generated by json-schema-rs. Do not edit manually.\n\nuse serde::{Deserialize, Serialize};\n\n#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\npub struct Root {\n    pub x: String,\n}\n\n".to_string();
4585        assert_eq!(expected, actual);
4586    }
4587
4588    #[test]
4589    fn dedupe_full_same_shape_different_examples_two_structs() {
4590        let j1 = r#"{"type":"object","properties":{"id":{"type":"string"}},"examples":[1]}"#;
4591        let j2 = r#"{"type":"object","properties":{"id":{"type":"string"}},"examples":[2]}"#;
4592        let s1: JsonSchema = serde_json::from_str(j1).unwrap();
4593        let s2: JsonSchema = serde_json::from_str(j2).unwrap();
4594        let settings: CodeGenSettings = CodeGenSettings::builder()
4595            .dedupe_mode(DedupeMode::Full)
4596            .build();
4597        let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4598        let expected: (bool, usize) = (false, 2);
4599        let actual: (bool, usize) = (output.shared.is_some(), output.per_schema.len());
4600        assert_eq!(expected, actual);
4601    }
4602
4603    #[test]
4604    fn dedupe_functional_same_shape_different_examples_one_struct() {
4605        let j1 = r#"{"type":"object","properties":{"id":{"type":"string"}},"examples":[1]}"#;
4606        let j2 = r#"{"type":"object","properties":{"id":{"type":"string"}},"examples":[2]}"#;
4607        let s1: JsonSchema = serde_json::from_str(j1).unwrap();
4608        let s2: JsonSchema = serde_json::from_str(j2).unwrap();
4609        let settings: CodeGenSettings = CodeGenSettings::builder()
4610            .dedupe_mode(DedupeMode::Functional)
4611            .build();
4612        let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4613        let expected_shared_some = true;
4614        let actual_shared_some = output.shared.is_some();
4615        assert_eq!(expected_shared_some, actual_shared_some);
4616    }
4617
4618    #[test]
4619    fn dedupe_full_same_shape_different_id_two_structs() {
4620        let j1 = r#"{"$id":"http://example.com/a","type":"object","properties":{"x":{"type":"string"}}}"#;
4621        let j2 = r#"{"$id":"http://example.com/b","type":"object","properties":{"x":{"type":"string"}}}"#;
4622        let s1: JsonSchema = serde_json::from_str(j1).unwrap();
4623        let s2: JsonSchema = serde_json::from_str(j2).unwrap();
4624        let settings: CodeGenSettings = CodeGenSettings::builder()
4625            .dedupe_mode(DedupeMode::Full)
4626            .build();
4627        let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4628        let expected_shared_some = false;
4629        let actual_shared_some = output.shared.is_some();
4630        let msg: &str = concat!(
4631            "Full dedupe: same shape different id yields two ",
4632            "structs"
4633        );
4634        assert_eq!(expected_shared_some, actual_shared_some, "{msg}");
4635    }
4636
4637    #[test]
4638    fn dedupe_functional_same_shape_different_id_one_struct() {
4639        let j1 = r#"{"$id":"http://example.com/a","type":"object","properties":{"x":{"type":"string"}}}"#;
4640        let j2 = r#"{"$id":"http://example.com/b","type":"object","properties":{"x":{"type":"string"}}}"#;
4641        let s1: JsonSchema = serde_json::from_str(j1).unwrap();
4642        let s2: JsonSchema = serde_json::from_str(j2).unwrap();
4643        let settings: CodeGenSettings = CodeGenSettings::builder()
4644            .dedupe_mode(DedupeMode::Functional)
4645            .build();
4646        let output: super::GenerateRustOutput = generate_rust(&[s1, s2], &settings).unwrap();
4647        let expected_shared_some = true;
4648        let actual_shared_some = output.shared.is_some();
4649        let msg: &str = concat!(
4650            "Functional dedupe: same shape different id yields one shared ",
4651            "struct"
4652        );
4653        assert_eq!(expected_shared_some, actual_shared_some, "{msg}");
4654    }
4655
4656    #[test]
4657    fn dedupe_functional_same_as_full_when_no_description() {
4658        let json = r#"{"type":"object","properties":{"id":{"type":"string"}}}"#;
4659        let s1: JsonSchema = serde_json::from_str(json).unwrap();
4660        let s2: JsonSchema = serde_json::from_str(json).unwrap();
4661        let settings_full: CodeGenSettings = CodeGenSettings::builder()
4662            .dedupe_mode(DedupeMode::Full)
4663            .build();
4664        let settings_func: CodeGenSettings = CodeGenSettings::builder()
4665            .dedupe_mode(DedupeMode::Functional)
4666            .build();
4667        let output_full: super::GenerateRustOutput =
4668            generate_rust(&[s1.clone(), s2.clone()], &settings_full).unwrap();
4669        let output_func: super::GenerateRustOutput =
4670            generate_rust(&[s1, s2], &settings_func).unwrap();
4671        assert_eq!(output_full.shared.is_some(), output_func.shared.is_some());
4672        assert_eq!(output_full.per_schema.len(), output_func.per_schema.len());
4673    }
4674
4675    #[test]
4676    fn default_settings_use_full_dedupe() {
4677        let settings: CodeGenSettings = CodeGenSettings::builder().build();
4678        let expected = DedupeMode::Full;
4679        let actual = settings.dedupe_mode;
4680        assert_eq!(expected, actual);
4681    }
4682
4683    #[cfg(feature = "uuid")]
4684    #[test]
4685    fn uuid_required_property() {
4686        let json = r#"{"type":"object","properties":{"id":{"type":"string","format":"uuid"}},"required":["id"]}"#;
4687        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4688        let settings: CodeGenSettings = default_settings();
4689        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4690        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4691        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
4692
4693use serde::{Deserialize, Serialize};
4694use uuid::Uuid;
4695
4696#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4697pub struct Root {
4698    pub id: Uuid,
4699}
4700
4701";
4702        assert_eq!(expected, actual);
4703    }
4704
4705    #[cfg(feature = "uuid")]
4706    #[test]
4707    fn uuid_optional_property() {
4708        let json = r#"{"type":"object","properties":{"id":{"type":"string","format":"uuid"}}}"#;
4709        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4710        let settings: CodeGenSettings = default_settings();
4711        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4712        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4713        let expected = r"//! Generated by json-schema-rs. Do not edit manually.
4714
4715use serde::{Deserialize, Serialize};
4716use uuid::Uuid;
4717
4718#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
4719pub struct Root {
4720    pub id: Option<Uuid>,
4721}
4722
4723";
4724        assert_eq!(expected, actual);
4725    }
4726
4727    #[cfg(feature = "uuid")]
4728    #[test]
4729    fn uuid_array_items() {
4730        let json = r#"{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["ids"]}"#;
4731        let schema: JsonSchema = serde_json::from_str(json).unwrap();
4732        let settings: CodeGenSettings = default_settings();
4733        let output: super::GenerateRustOutput = generate_rust(&[schema], &settings).unwrap();
4734        let actual = String::from_utf8(output.per_schema[0].clone()).unwrap();
4735        let expected = "//! Generated by json-schema-rs. Do not edit manually.\n\nuse serde::{\"Deserialize\", \"Serialize\"};\nuse uuid::Uuid;\n\n#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\npub struct Root {\n    pub ids: Vec<Uuid>,\n}\n\n";
4736        assert_eq!(expected, actual);
4737    }
4738}