Skip to main content

semiont_codegen/
types.rs

1//! Rust types from component schemas (JSON Schema draft 7, as
2//! `draft7_definitions` writes them): a struct per object, an enum per string
3//! enumeration, an untagged enum per `oneOf` or `anyOf`, an alias per named
4//! string, a type of its own per kind of id, a field per property, optional
5//! where the schema does not require it. It knows the shapes the spec's schemas use and refuses any other, so a
6//! schema that grows a new shape fails the build rather than generating
7//! something wrong.
8//!
9//! A union is untagged, and the first member that decodes is the one meant.
10//! Its members are told apart by what they are — text, a list, an object — or,
11//! between objects, by a single-valued discriminant each carries (`status`,
12//! `kind`, `code`) or by one being the empty object. What the schema says that a type cannot — a pattern, a length, a
13//! bound — is the validators' to hold at the boundary, before a value is
14//! decoded. A kind of id is the exception: its pattern is written as the
15//! check its only constructor makes, and decoding goes through that
16//! constructor, so a value of the type has always passed its rule.
17
18use serde_json::Value;
19use std::collections::BTreeMap;
20use std::fmt::Write as _;
21
22/// What one generation writes.
23pub struct Generation<'a> {
24    /// The schemas generated, and every schema they reach unless `elsewhere` is set.
25    pub roots: &'a [&'a str],
26    /// With a path, only the roots are generated, and every other schema they
27    /// reach is named as `<path>::<Name>`: the crate that owns it.
28    pub elsewhere: Option<&'a str>,
29    /// The schemas that are kinds of id (specs/src/identifiers/kinds.json).
30    /// Each is a type of its own and not an alias, and a value of it is made
31    /// only by a constructor that holds it to the schema's pattern.
32    pub identifiers: &'a [&'a str],
33}
34
35const EMPTY_OBJECT: &str = "EmptyObject";
36/// The key the `stated` helper is written under: no schema is named so.
37const STATED: &str = "fn stated";
38/// What a constructor of a kind of id refuses with.
39const INVALID_IDENTIFIER: &str = "InvalidIdentifier";
40
41/// The Rust source of a generation's types, from `definitions` (the
42/// `definitions` object of `draft7_definitions`' output).
43pub fn generate(definitions: &Value, generation: &Generation<'_>) -> String {
44    let mut types = Types {
45        definitions,
46        generation,
47        written: BTreeMap::new(),
48    };
49    for root in generation.roots {
50        types.named(root);
51    }
52    let mut out =
53        String::from("// Generated from the component schemas in specs/src; do not edit.\n");
54    for code in types.written.values() {
55        out.push_str(code);
56    }
57    out
58}
59
60struct Types<'a> {
61    definitions: &'a Value,
62    generation: &'a Generation<'a>,
63    /// Every type written, by name, so a schema reached twice is written once.
64    written: BTreeMap<String, String>,
65}
66
67impl Types<'_> {
68    fn schema(&self, name: &str) -> &Value {
69        self.definitions
70            .get(name)
71            .unwrap_or_else(|| panic!("the spec declares no schema {name}"))
72    }
73
74    /// The type a component schema names: written here, or named where it lives.
75    fn named(&mut self, name: &str) -> String {
76        let owned = self.generation.elsewhere.is_none() || self.generation.roots.contains(&name);
77        if !owned {
78            return format!(
79                "{}::{name}",
80                self.generation.elsewhere.expect("checked above")
81            );
82        }
83        if !self.written.contains_key(name) {
84            let schema = self.schema(name).clone();
85            self.written.insert(name.to_owned(), String::new());
86            let code = self.declaration(name, &schema);
87            self.written.insert(name.to_owned(), code);
88        }
89        name.to_owned()
90    }
91
92    /// The name of the type of `owner`'s property `property`, when its schema
93    /// is written in place: the two names together, and `Value` after them
94    /// for as long as a component schema already has that name.
95    fn inline_name(&self, owner: &str, property: &str) -> String {
96        let mut name = format!("{owner}{}", pascal(property));
97        while self.definitions.get(&name).is_some() {
98            name.push_str("Value");
99        }
100        name
101    }
102
103    fn inline(&mut self, name: &str, schema: &Value) -> String {
104        let code = self.declaration(name, schema);
105        self.written.insert(name.to_owned(), code);
106        name.to_owned()
107    }
108
109    fn empty_object(&mut self) -> String {
110        self.written.entry(EMPTY_OBJECT.to_owned()).or_insert_with(|| {
111            "/// An object with no properties.\n#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize, serde::Serialize)]\n#[serde(deny_unknown_fields)]\npub struct EmptyObject {}\n\n".to_owned()
112        });
113        EMPTY_OBJECT.to_owned()
114    }
115
116    /// Whether a schema is a string, following `$ref`s and unions of strings.
117    fn is_string(&self, schema: &Value) -> bool {
118        if let Some(name) = reference(schema) {
119            return self.is_string(self.schema(name));
120        }
121        if let Some(members) = union(schema) {
122            return members.iter().all(|m| self.is_string(m));
123        }
124        schema["type"] == "string" && schema.get("enum").is_none() && schema.get("const").is_none()
125    }
126
127    /// The Rust type of a value `owner`'s property `property` holds.
128    fn type_of(&mut self, owner: &str, property: &str, schema: &Value) -> String {
129        if let Some(name) = reference(schema) {
130            return self.named(name);
131        }
132        if schema.as_object().is_some_and(|o| o.is_empty()) {
133            return "serde_json::Value".to_owned();
134        }
135        if let Some(inner) = without_null(schema) {
136            return self.type_of(owner, property, &inner);
137        }
138        // An `allOf` of one schema is that schema: the form a `$ref` takes
139        // when something is said beside it.
140        if let Some([only]) = schema["allOf"].as_array().map(Vec::as_slice)
141            && reference(only).is_some()
142            && schema
143                .as_object()
144                .is_some_and(|o| o.keys().all(|k| k == "allOf" || k == "description"))
145        {
146            return self.type_of(owner, property, only);
147        }
148        if schema.get("allOf").is_some() {
149            return self.inline(&self.inline_name(owner, property), schema);
150        }
151        if let Some(members) = union(schema) {
152            if members.iter().all(|m| self.is_string(m)) {
153                return "String".to_owned();
154            }
155            return self.inline(&self.inline_name(owner, property), schema);
156        }
157        match schema["type"].as_str() {
158            Some("string") if schema.get("enum").is_some() || schema.get("const").is_some() => {
159                self.inline(&self.inline_name(owner, property), schema)
160            }
161            Some("string") => "String".to_owned(),
162            Some("boolean") => "bool".to_owned(),
163            Some("number") => "f64".to_owned(),
164            Some("integer") => {
165                let minimum = schema["minimum"].as_i64();
166                let maximum = schema["maximum"].as_i64();
167                match (minimum, maximum) {
168                    (Some(min), Some(max)) if min >= 0 && max <= i64::from(u16::MAX) => "u16",
169                    (Some(min), _) if min >= 0 => "u64",
170                    _ => "i64",
171                }
172                .to_owned()
173            }
174            Some("array") => {
175                let item = self.type_of(owner, &format!("{property}Item"), &schema["items"]);
176                format!("Vec<{item}>")
177            }
178            Some("object") => {
179                let properties = schema["properties"].as_object().filter(|p| !p.is_empty());
180                match (properties, &schema["additionalProperties"]) {
181                    (Some(_), _) => self.inline(&self.inline_name(owner, property), schema),
182                    (None, Value::Bool(false)) if schema["maxProperties"] == 0 => {
183                        self.empty_object()
184                    }
185                    (None, Value::Bool(false)) => {
186                        self.inline(&self.inline_name(owner, property), schema)
187                    }
188                    (None, Value::Null | Value::Bool(true)) if schema["maxProperties"] == 0 => {
189                        self.empty_object()
190                    }
191                    (None, Value::Null | Value::Bool(true)) => {
192                        "serde_json::Map<String, serde_json::Value>".to_owned()
193                    }
194                    (None, values) => {
195                        let value = self.type_of(owner, &format!("{property}Value"), values);
196                        format!("std::collections::BTreeMap<String, {value}>")
197                    }
198                }
199            }
200            _ => panic!("{owner}.{property}: a schema shape the generator does not know: {schema}"),
201        }
202    }
203
204    fn declaration(&mut self, name: &str, schema: &Value) -> String {
205        let mut code = String::new();
206        doc(&mut code, "", schema);
207        if let Some(members) = union(schema) {
208            return self.union_declaration(name, members, code);
209        }
210        let merged;
211        let schema = match schema["allOf"].as_array() {
212            Some(members) => {
213                merged = self.merged(name, members);
214                &merged
215            }
216            None => schema,
217        };
218        let constant = schema.get("const").map(|value| vec![value.clone()]);
219        if let Some(values) = schema["enum"].as_array().or(constant.as_ref()) {
220            if schema["type"] != "string" {
221                panic!("{name}: only string enumerations are generated");
222            }
223            code.push_str("#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)]\n");
224            let _ = writeln!(code, "pub enum {name} {{");
225            let mut taken: Vec<String> = Vec::new();
226            let mut spellings = String::new();
227            for value in values {
228                let value = value
229                    .as_str()
230                    .unwrap_or_else(|| panic!("{name}: an enum value is not a string"));
231                // A `+` is part of what a value says (`text/x-c++` is not
232                // `text/x-c`), so it is spelled rather than dropped.
233                let variant = pascal(&value.replace('+', " plus "));
234                if taken.contains(&variant) {
235                    panic!("{name}: two enum values would both be the variant {variant}");
236                }
237                let _ = writeln!(code, "    #[serde(rename = \"{value}\")]\n    {variant},");
238                let _ = writeln!(spellings, "            {name}::{variant} => {value:?},");
239                taken.push(variant);
240            }
241            code.push_str("}\n\n");
242            let _ = writeln!(
243                code,
244                "impl {name} {{\n    /// The value as the wire spells it.\n    pub const fn as_str(&self) -> &'static str {{\n        match self {{\n{spellings}        }}\n    }}\n}}\n"
245            );
246            return code;
247        }
248        if self.generation.identifiers.contains(&name) {
249            return self.identifier(name, schema, code);
250        }
251        if self.is_string(schema) {
252            let _ = writeln!(code, "pub type {name} = String;\n");
253            return code;
254        }
255        if schema["type"] != "object" {
256            panic!("{name}: a schema shape the generator does not know: {schema}");
257        }
258        let empty = serde_json::Map::new();
259        let properties = schema["properties"].as_object().unwrap_or(&empty);
260        let required: Vec<&str> = schema["required"]
261            .as_array()
262            .map(|r| r.iter().filter_map(Value::as_str).collect())
263            .unwrap_or_default();
264        let mut fields = std::collections::BTreeSet::new();
265        for property in properties.keys() {
266            if !fields.insert(snake(property)) {
267                panic!(
268                    "{name}: two properties are named {} in Rust",
269                    snake(property)
270                );
271            }
272        }
273        let open = match &schema["additionalProperties"] {
274            Value::Bool(true) => true,
275            Value::Bool(false) | Value::Null => false,
276            other => panic!(
277                "{name}: properties beside additionalProperties {other}, which the generator does not know"
278            ),
279        };
280        code.push_str("#[derive(Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)]\n");
281        if schema["additionalProperties"] == false {
282            code.push_str("#[serde(deny_unknown_fields)]\n");
283        }
284        let _ = writeln!(code, "pub struct {name} {{");
285        for (property, property_schema) in properties {
286            let rust_type = self.type_of(name, property, property_schema);
287            doc(&mut code, "    ", property_schema);
288            let field = snake(property);
289            if field.trim_start_matches("r#") != property.as_str() {
290                let _ = writeln!(code, "    #[serde(rename = \"{property}\")]");
291            }
292            let nullable = without_null(property_schema).is_some();
293            if required.contains(&property.as_str()) && nullable {
294                let _ = writeln!(code, "    pub {field}: Option<{rust_type}>,");
295            } else if required.contains(&property.as_str()) {
296                let _ = writeln!(code, "    pub {field}: {rust_type},");
297            } else if nullable {
298                // Absent, null and a value are three things: the outer option
299                // is whether it was stated, the inner whether it was null.
300                let _ = writeln!(
301                    code,
302                    "    #[serde(default, deserialize_with = \"stated\", skip_serializing_if = \"Option::is_none\")]\n    pub {field}: Option<Option<{rust_type}>>,"
303                );
304                self.written.entry(STATED.to_owned()).or_insert_with(|| {
305                    "/// A property that was stated, whatever it stated: null is `Some(None)`.\nfn stated<'de, T: serde::Deserialize<'de>, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Option<Option<T>>, D::Error> {\n    serde::Deserialize::deserialize(deserializer).map(Some)\n}\n\n".to_owned()
306                });
307            } else {
308                let _ = writeln!(
309                    code,
310                    "    #[serde(default, skip_serializing_if = \"Option::is_none\")]\n    pub {field}: Option<{rust_type}>,"
311                );
312            }
313        }
314        if open {
315            code.push_str("    /// Every property the schema does not name, as it came.\n    #[serde(flatten)]\n    pub rest: serde_json::Map<String, serde_json::Value>,\n");
316        }
317        code.push_str("}\n\n");
318        code
319    }
320
321    /// A kind of id: a string no code can make but through `new`, which
322    /// holds it to the schema's pattern. Decoding goes through `new` too, so
323    /// an id a peer sent is held to the same rule as one a caller made. It
324    /// reads as the text it is (`Deref`), and nothing turns text into it but
325    /// the constructor.
326    fn identifier(&mut self, name: &str, schema: &Value, mut code: String) -> String {
327        if self.definitions.get(INVALID_IDENTIFIER).is_some() {
328            panic!("{INVALID_IDENTIFIER} is a schema's name, and the generator's own");
329        }
330        if schema["type"] != "string" {
331            panic!("{name}: a kind of id that is not a string: {schema}");
332        }
333        let pattern = schema["pattern"]
334            .as_str()
335            .unwrap_or_else(|| panic!("{name}: a kind of id states no pattern"));
336        let check = Rule::of(pattern)
337            .unwrap_or_else(|| {
338                panic!("{name}: a pattern the generator cannot write as a check: {pattern}")
339            })
340            .check();
341        self.written.entry(INVALID_IDENTIFIER.to_owned()).or_insert_with(|| {
342            "/// A string that is not an id of the kind it was to be.\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct InvalidIdentifier {\n    /// The kind it was to be.\n    pub kind: &'static str,\n    /// The rule of that kind, as the spec writes it.\n    pub pattern: &'static str,\n    pub value: String,\n}\n\nimpl std::fmt::Display for InvalidIdentifier {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{:?} is not a {}: it does not match {}\", self.value, self.kind, self.pattern)\n    }\n}\n\nimpl std::error::Error for InvalidIdentifier {}\n\n".to_owned()
343        });
344        code.push_str("#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)]\n#[serde(try_from = \"String\", into = \"String\")]\n");
345        let _ = writeln!(code, "pub struct {name}(String);\n");
346        let _ = writeln!(
347            code,
348            "impl {name} {{\n    /// The rule a value is held to, as the spec writes it.\n    pub const PATTERN: &'static str = {pattern:?};\n\n    /// `value` as a `{name}`, or that it is not one.\n    pub fn new(value: impl Into<String>) -> Result<{name}, InvalidIdentifier> {{\n        let value = value.into();\n        if {name}::admits(&value) {{\n            Ok({name}(value))\n        }} else {{\n            Err(InvalidIdentifier {{\n                kind: {name:?},\n                pattern: {name}::PATTERN,\n                value,\n            }})\n        }}\n    }}\n\n    /// Whether `value` passes the rule.\n    pub fn admits(value: &str) -> bool {{\n{check}    }}\n\n    pub fn as_str(&self) -> &str {{\n        &self.0\n    }}\n}}\n"
349        );
350        let _ = writeln!(
351            code,
352            "impl std::fmt::Display for {name} {{\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{\n        f.write_str(&self.0)\n    }}\n}}\n\nimpl std::ops::Deref for {name} {{\n    type Target = str;\n\n    fn deref(&self) -> &str {{\n        &self.0\n    }}\n}}\n\nimpl AsRef<str> for {name} {{\n    fn as_ref(&self) -> &str {{\n        &self.0\n    }}\n}}\n\nimpl std::str::FromStr for {name} {{\n    type Err = InvalidIdentifier;\n\n    fn from_str(value: &str) -> Result<{name}, InvalidIdentifier> {{\n        {name}::new(value)\n    }}\n}}\n\nimpl TryFrom<String> for {name} {{\n    type Error = InvalidIdentifier;\n\n    fn try_from(value: String) -> Result<{name}, InvalidIdentifier> {{\n        {name}::new(value)\n    }}\n}}\n\nimpl From<{name}> for String {{\n    fn from(id: {name}) -> String {{\n        id.0\n    }}\n}}\n\nimpl PartialEq<str> for {name} {{\n    fn eq(&self, other: &str) -> bool {{\n        self.0 == other\n    }}\n}}\n\nimpl PartialEq<&str> for {name} {{\n    fn eq(&self, other: &&str) -> bool {{\n        self.0 == *other\n    }}\n}}\n"
353        );
354        code
355    }
356
357    /// An `allOf`'s members as one object: the properties of each, a later
358    /// member's refining an earlier's, and every member's requirements.
359    fn merged(&self, name: &str, members: &[Value]) -> Value {
360        let mut properties = serde_json::Map::new();
361        let mut required: Vec<Value> = Vec::new();
362        let mut closed = false;
363        for member in members {
364            let member = match reference(member) {
365                Some(target) => self.schema(target).clone(),
366                None => member.clone(),
367            };
368            if member["type"] != "object" {
369                panic!("{name}: an allOf member that is not an object: {member}");
370            }
371            if let Some(own) = member["properties"].as_object() {
372                properties.extend(own.clone());
373            }
374            if let Some(own) = member["required"].as_array() {
375                required.extend(
376                    own.iter()
377                        .filter(|r| !required.contains(r))
378                        .cloned()
379                        .collect::<Vec<_>>(),
380                );
381            }
382            closed |= member["additionalProperties"] == false;
383        }
384        let mut object =
385            serde_json::json!({ "type": "object", "properties": properties, "required": required });
386        if closed {
387            object["additionalProperties"] = Value::Bool(false);
388        }
389        object
390    }
391
392    fn union_declaration(&mut self, name: &str, members: &[Value], mut code: String) -> String {
393        let referenced: Vec<Option<&str>> = members.iter().map(reference).collect();
394        let prefix = common_prefix(referenced.iter().flatten().copied());
395        let mut variants = Vec::new();
396        for member in members {
397            let (variant, rust_type) = match reference(member) {
398                Some(target) => {
399                    let rust_type = self.named(target);
400                    let short = target.strip_prefix(prefix.as_str()).unwrap_or(target);
401                    (short.to_owned(), rust_type)
402                }
403                None if member["type"] == "object" && member["maxProperties"] == 0 => {
404                    ("Empty".to_owned(), self.empty_object())
405                }
406                None if self.is_string(member) => ("Text".to_owned(), "String".to_owned()),
407                None if member["type"] == "array" => {
408                    let item = self.type_of(name, "Item", &member["items"]);
409                    ("List".to_owned(), format!("Vec<{item}>"))
410                }
411                None if member["type"] == "object" => {
412                    // An inline member is named by the one value its
413                    // discriminant takes, when it has one.
414                    let variant = discriminant(member).map_or("Object".to_owned(), pascal);
415                    let rust_type = self.type_of(name, &variant, member);
416                    (variant, rust_type)
417                }
418                None => panic!("{name}: a union member the generator does not know: {member}"),
419            };
420            if variants.iter().any(|(taken, _)| *taken == variant) {
421                panic!("{name}: two union members would both be the variant {variant}");
422            }
423            variants.push((variant, rust_type));
424        }
425        code.push_str("#[derive(Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)]\n#[serde(untagged)]\n");
426        let _ = writeln!(code, "pub enum {name} {{");
427        for (variant, rust_type) in variants {
428            let _ = writeln!(code, "    {variant}({rust_type}),");
429        }
430        code.push_str("}\n\n");
431        code
432    }
433}
434
435/// A kind of id's pattern, as the check that holds a value to it. The
436/// patterns it knows are the ones the kinds use: anchored at both ends, text
437/// every value begins with, and then one set of characters a stated number
438/// of times. Anything else is `None`, and the build fails rather than link a
439/// regular-expression engine into every client for four rules.
440struct Rule {
441    prefix: String,
442    /// The characters admitted after the prefix, as a predicate over `c`.
443    admitted: String,
444    least: usize,
445    most: Option<usize>,
446}
447
448impl Rule {
449    fn of(pattern: &str) -> Option<Rule> {
450        let body = pattern.strip_prefix('^')?.strip_suffix('$')?;
451        let set_at = body.find(['[', '\\'])?;
452        let (prefix, rest) = body.split_at(set_at);
453        if prefix.contains(['.', '|', '?', '*', '+', '(', ')', '{', '}', '^', '$', ']']) {
454            return None;
455        }
456        let (admitted, times) = match rest.strip_prefix("\\S") {
457            Some(times) => ("!c.is_whitespace()".to_owned(), times),
458            None => {
459                let (set, times) = rest.strip_prefix('[')?.split_once(']')?;
460                (Rule::set(set)?, times)
461            }
462        };
463        let (least, most) = match times {
464            "+" => (1, None),
465            "*" => (0, None),
466            _ => {
467                let bounds = times.strip_prefix('{')?.strip_suffix('}')?;
468                match bounds.split_once(',') {
469                    Some((least, "")) => (least.parse().ok()?, None),
470                    Some((least, most)) => (least.parse().ok()?, Some(most.parse().ok()?)),
471                    None => (bounds.parse().ok()?, Some(bounds.parse().ok()?)),
472                }
473            }
474        };
475        Some(Rule {
476            prefix: prefix.to_owned(),
477            admitted,
478            least,
479            most,
480        })
481    }
482
483    /// A character set (`A-Za-z0-9_-`) as a `matches!` over `c`: ranges and
484    /// single characters, a `-` at either end being itself.
485    fn set(set: &str) -> Option<String> {
486        if set.is_empty() || set.starts_with('^') || set.contains(['\\', '[', '\'']) {
487            return None;
488        }
489        let characters: Vec<char> = set.chars().collect();
490        let mut arms = Vec::new();
491        let mut at = 0;
492        while at < characters.len() {
493            if at + 2 < characters.len() && characters[at + 1] == '-' {
494                arms.push(format!("'{}'..='{}'", characters[at], characters[at + 2]));
495                at += 3;
496            } else {
497                arms.push(format!("'{}'", characters[at]));
498                at += 1;
499            }
500        }
501        Some(format!("matches!(c, {})", arms.join(" | ")))
502    }
503
504    /// The body of `fn admits(value: &str) -> bool`.
505    fn check(&self) -> String {
506        let mut code = String::new();
507        if !self.prefix.is_empty() {
508            let _ = writeln!(
509                code,
510                "        let Some(value) = value.strip_prefix({:?}) else {{\n            return false;\n        }};",
511                self.prefix
512            );
513        }
514        let counted = match (self.least, self.most) {
515            (0, None) => None,
516            (least, None) => Some(format!("value.chars().count() >= {least}")),
517            (least, Some(most)) => Some(format!(
518                "({least}..={most}).contains(&value.chars().count())"
519            )),
520        };
521        let all = format!("value.chars().all(|c| {})", self.admitted);
522        match counted {
523            Some(counted) => {
524                let _ = writeln!(code, "        {counted}\n            && {all}");
525            }
526            None => {
527                let _ = writeln!(code, "        {all}");
528            }
529        }
530        code
531    }
532}
533
534/// A schema that also admits null (`type: [T, "null"]` or a union with a
535/// `null` member, draft 7's forms of OpenAPI's `nullable`), as the schema
536/// without it.
537fn without_null(schema: &Value) -> Option<Value> {
538    if let Some(members) = union(schema) {
539        let others: Vec<&Value> = members.iter().filter(|m| m["type"] != "null").collect();
540        return match others.as_slice() {
541            _ if others.len() == members.len() => None,
542            [only] => Some((*only).clone()),
543            _ => Some(serde_json::json!({ "anyOf": others })),
544        };
545    }
546    let types = schema["type"].as_array()?;
547    let others: Vec<&Value> = types.iter().filter(|t| *t != "null").collect();
548    if others.len() != 1 || others.len() == types.len() {
549        panic!("a schema of several types the generator does not know: {schema}");
550    }
551    let mut inner = schema.clone();
552    inner["type"] = others[0].clone();
553    Some(inner)
554}
555
556/// The single value an object's discriminating property takes: the first of
557/// its properties that admits exactly one string.
558fn discriminant(object: &Value) -> Option<&str> {
559    object["properties"]
560        .as_object()?
561        .values()
562        .find_map(|property| {
563            match (
564                property["enum"].as_array().map(Vec::as_slice),
565                &property["const"],
566            ) {
567                (Some([only]), _) => only.as_str(),
568                (None, Value::String(only)) => Some(only.as_str()),
569                _ => None,
570            }
571        })
572}
573
574fn reference(schema: &Value) -> Option<&str> {
575    schema["$ref"].as_str().map(|r| {
576        r.strip_prefix("#/definitions/")
577            .unwrap_or_else(|| panic!("$ref {r} is not a component schema"))
578    })
579}
580
581fn union(schema: &Value) -> Option<&[Value]> {
582    schema["oneOf"]
583        .as_array()
584        .or_else(|| schema["anyOf"].as_array())
585        .map(Vec::as_slice)
586}
587
588/// The longest prefix every name shares that ends where a word ends, and leaves
589/// each name a word of its own: `JobPending`, `JobRunning` share `Job`.
590fn common_prefix<'a>(names: impl Iterator<Item = &'a str>) -> String {
591    let names: Vec<&str> = names.collect();
592    let Some(first) = names.first() else {
593        return String::new();
594    };
595    if names.len() < 2 {
596        return String::new();
597    }
598    let mut prefix = String::new();
599    for (index, c) in first.char_indices() {
600        let candidate = &first[..index];
601        if c.is_ascii_uppercase()
602            && !candidate.is_empty()
603            && names.iter().all(|n| {
604                n.starts_with(candidate)
605                    && n[candidate.len()..].starts_with(|c: char| c.is_ascii_uppercase())
606            })
607        {
608            prefix = candidate.to_owned();
609        }
610    }
611    prefix
612}
613
614/// A schema's description as doc comments, indented.
615fn doc(code: &mut String, indent: &str, schema: &Value) {
616    if let Some(description) = schema["description"].as_str() {
617        for line in description.lines() {
618            let _ = writeln!(code, "{indent}/// {line}");
619        }
620    }
621}
622
623/// `in-process` and `subjectClaim` as `InProcess` and `SubjectClaim`.
624pub fn pascal(word: &str) -> String {
625    let mut out = String::new();
626    let mut upper = true;
627    for c in word.chars() {
628        if c.is_ascii_alphanumeric() {
629            if upper {
630                out.push(c.to_ascii_uppercase());
631            } else {
632                out.push(c);
633            }
634            upper = false;
635        } else {
636            upper = true;
637        }
638    }
639    out
640}
641
642/// `subjectClaim` as `subject_claim`; a Rust keyword raw.
643pub fn snake(word: &str) -> String {
644    let mut out = String::new();
645    // JSON-LD's keywords (`@id`, `@type`) are named without their `@`.
646    for c in word.trim_start_matches('@').chars() {
647        if c.is_ascii_uppercase() {
648            out.push('_');
649            out.push(c.to_ascii_lowercase());
650        } else if c.is_ascii_alphanumeric() || c == '_' {
651            out.push(c);
652        } else {
653            out.push('_');
654        }
655    }
656    if matches!(
657        out.as_str(),
658        "type"
659            | "ref"
660            | "match"
661            | "use"
662            | "mod"
663            | "move"
664            | "self"
665            | "crate"
666            | "struct"
667            | "enum"
668            | "fn"
669    ) {
670        format!("r#{out}")
671    } else {
672        out
673    }
674}
675
676#[cfg(test)]
677mod tests {
678    use super::*;
679    use serde_json::json;
680
681    fn generated(definitions: Value, root: &str) -> String {
682        generate(
683            &definitions,
684            &Generation {
685                roots: &[root],
686                elsewhere: None,
687                identifiers: &[],
688            },
689        )
690    }
691
692    fn identifier(pattern: &str) -> String {
693        generate(
694            &json!({ "ThingId": { "type": "string", "description": "A thing's id.", "pattern": pattern } }),
695            &Generation {
696                roots: &["ThingId"],
697                elsewhere: None,
698                identifiers: &["ThingId"],
699            },
700        )
701    }
702
703    #[test]
704    fn a_kind_of_id_is_a_type_of_its_own_that_decodes_through_its_constructor() {
705        let code = identifier("^[A-Za-z0-9_-]{1,128}$");
706        assert!(!code.contains("pub type ThingId"), "{code}");
707        assert!(
708            code.contains("/// A thing's id.\n#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)]\n#[serde(try_from = \"String\", into = \"String\")]\npub struct ThingId(String);"),
709            "{code}"
710        );
711        assert!(
712            code.contains(
713                "pub fn new(value: impl Into<String>) -> Result<ThingId, InvalidIdentifier>"
714            ),
715            "{code}"
716        );
717        assert!(code.contains("pub struct InvalidIdentifier"), "{code}");
718        // Read as the text it is, wherever text is wanted.
719        assert!(
720            code.contains("impl std::ops::Deref for ThingId {\n    type Target = str;"),
721            "{code}"
722        );
723    }
724
725    #[test]
726    fn a_kind_of_ids_rule_is_written_as_its_check() {
727        let name = identifier("^[A-Za-z0-9_-]{1,128}$");
728        assert!(
729            name.contains("(1..=128).contains(&value.chars().count())\n            && value.chars().all(|c| matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-'))"),
730            "{name}"
731        );
732        assert!(
733            name.contains("pub const PATTERN: &'static str = \"^[A-Za-z0-9_-]{1,128}$\";"),
734            "{name}"
735        );
736        let did = identifier("^did:\\S+$");
737        assert!(
738            did.contains("let Some(value) = value.strip_prefix(\"did:\") else {\n            return false;\n        };\n        value.chars().count() >= 1\n            && value.chars().all(|c| !c.is_whitespace())"),
739            "{did}"
740        );
741    }
742
743    #[test]
744    #[should_panic(expected = "ThingId: a pattern the generator cannot write as a check")]
745    fn a_kind_of_id_whose_rule_cannot_be_written_as_a_check_is_refused() {
746        identifier("^e-[^:]+:[^:]+:.+$");
747    }
748
749    #[test]
750    #[should_panic(expected = "ThingId: a kind of id states no pattern")]
751    fn a_kind_of_id_with_no_rule_is_refused() {
752        generate(
753            &json!({ "ThingId": { "type": "string" } }),
754            &Generation {
755                roots: &["ThingId"],
756                elsewhere: None,
757                identifiers: &["ThingId"],
758            },
759        );
760    }
761
762    #[test]
763    fn a_property_that_refers_to_a_kind_of_id_is_of_its_type() {
764        let code = generate(
765            &json!({
766                "ThingId": { "type": "string", "pattern": "^[a-z]+$" },
767                "Holder": { "type": "object", "required": ["thing"], "properties": {
768                    "thing": { "$ref": "#/definitions/ThingId" },
769                    "others": { "type": "array", "items": { "$ref": "#/definitions/ThingId" } },
770                    "maybe": { "anyOf": [{ "$ref": "#/definitions/ThingId" }, { "type": "null" }] }
771                } }
772            }),
773            &Generation {
774                roots: &["Holder"],
775                elsewhere: None,
776                identifiers: &["ThingId"],
777            },
778        );
779        assert!(code.contains("    pub thing: ThingId,"), "{code}");
780        assert!(
781            code.contains("    pub others: Option<Vec<ThingId>>,"),
782            "{code}"
783        );
784        assert!(
785            code.contains("    pub maybe: Option<Option<ThingId>>,"),
786            "{code}"
787        );
788    }
789
790    #[test]
791    fn a_union_names_each_member_by_what_it_is() {
792        let code = generated(
793            json!({
794                "Target": { "type": "object", "properties": { "source": { "type": "string" } }, "required": ["source"] },
795                "Holder": { "type": "object", "required": ["target"], "properties": { "target": { "oneOf": [
796                    { "type": "string" },
797                    { "$ref": "#/definitions/Target" },
798                    { "type": "array", "items": { "$ref": "#/definitions/Target" } },
799                    { "type": "object", "additionalProperties": true }
800                ] } } }
801            }),
802            "Holder",
803        );
804        assert!(code.contains("pub enum HolderTarget {\n    Text(String),\n    Target(Target),\n    List(Vec<Target>),\n    Object(serde_json::Map<String, serde_json::Value>),\n}"), "{code}");
805    }
806
807    #[test]
808    fn inline_members_are_named_by_their_discriminants() {
809        let code = generated(
810            json!({ "Focus": { "oneOf": [
811                { "type": "object", "required": ["kind"], "properties": { "kind": { "type": "string", "enum": ["annotation"] } } },
812                { "type": "object", "required": ["kind"], "properties": { "kind": { "type": "string", "enum": ["resource"] } } }
813            ] } }),
814            "Focus",
815        );
816        assert!(code.contains("pub enum Focus {\n    Annotation(FocusAnnotation),\n    Resource(FocusResource),\n}"), "{code}");
817    }
818
819    #[test]
820    fn a_plus_in_an_enum_value_is_spelled_in_its_variant() {
821        let code = generated(
822            json!({ "Media": { "type": "string", "enum": ["text/x-c", "text/x-c++", "image/svg+xml"] } }),
823            "Media",
824        );
825        assert!(
826            code.contains("#[serde(rename = \"text/x-c\")]\n    TextXC,"),
827            "{code}"
828        );
829        assert!(
830            code.contains("#[serde(rename = \"text/x-c++\")]\n    TextXCPlusPlus,"),
831            "{code}"
832        );
833        assert!(
834            code.contains("#[serde(rename = \"image/svg+xml\")]\n    ImageSvgPlusXml,"),
835            "{code}"
836        );
837    }
838
839    #[test]
840    fn an_enum_says_each_value_as_the_wire_spells_it() {
841        let code = generated(
842            json!({ "Tone": { "type": "string", "enum": ["scholarly", "text/x-c++"] } }),
843            "Tone",
844        );
845        assert!(
846            code.contains(
847                "impl Tone {\n    /// The value as the wire spells it.\n    pub const fn as_str(&self) -> &'static str {\n        match self {\n            Tone::Scholarly => \"scholarly\",\n            Tone::TextXCPlusPlus => \"text/x-c++\",\n        }\n    }\n}"
848            ),
849            "{code}"
850        );
851    }
852
853    #[test]
854    #[should_panic(expected = "two enum values would both be the variant TextPlain")]
855    fn two_enum_values_one_variant_would_name_are_refused() {
856        generated(
857            json!({ "Media": { "type": "string", "enum": ["text/plain", "text-plain"] } }),
858            "Media",
859        );
860    }
861
862    #[test]
863    #[should_panic(expected = "two union members would both be the variant Text")]
864    fn two_members_nothing_tells_apart_are_refused() {
865        generated(
866            json!({ "Twice": { "oneOf": [
867                { "type": "string" },
868                { "type": "string", "description": "another" },
869                { "type": "array", "items": { "type": "string" } }
870            ] } }),
871            "Twice",
872        );
873    }
874
875    #[test]
876    fn a_named_string_is_an_alias() {
877        let code = generated(
878            json!({ "MediaType": { "type": "string", "description": "A MIME type." } }),
879            "MediaType",
880        );
881        assert!(
882            code.contains("/// A MIME type.\npub type MediaType = String;"),
883            "{code}"
884        );
885    }
886
887    #[test]
888    fn an_optional_nullable_property_keeps_absent_and_null_apart() {
889        let code = generated(
890            json!({ "Page": { "type": "object", "properties": { "cursor": { "type": ["string", "null"] } } } }),
891            "Page",
892        );
893        assert!(code.contains("deserialize_with = \"stated\""), "{code}");
894        assert!(
895            code.contains("pub cursor: Option<Option<String>>,"),
896            "{code}"
897        );
898        assert!(code.contains("fn stated<"), "{code}");
899    }
900
901    #[test]
902    fn a_nullable_reference_is_the_referenced_type_and_not_a_copy_of_it() {
903        let code = generated(
904            json!({
905                "Resource": { "type": "object", "properties": { "name": { "type": "string" } } },
906                "Answer": { "type": "object", "required": ["resource"], "properties": { "resource": { "anyOf": [
907                    { "type": "null" },
908                    { "allOf": [{ "$ref": "#/definitions/Resource" }], "description": "The resource, if any." }
909                ] } } }
910            }),
911            "Answer",
912        );
913        assert!(code.contains("pub resource: Option<Resource>,"), "{code}");
914        assert!(!code.contains("AnswerResource"), "{code}");
915    }
916
917    #[test]
918    fn a_type_written_in_place_takes_a_name_no_schema_has() {
919        let code = generated(
920            json!({
921                "NoteBody": { "type": "object", "properties": { "value": { "type": "string" } } },
922                "Note": { "type": "object", "properties": { "body": { "oneOf": [
923                    { "$ref": "#/definitions/NoteBody" },
924                    { "type": "array", "items": { "$ref": "#/definitions/NoteBody" } }
925                ] } } }
926            }),
927            "Note",
928        );
929        assert!(code.contains("pub struct NoteBody {"), "{code}");
930        assert!(
931            code.contains(
932                "pub enum NoteBodyValue {\n    NoteBody(NoteBody),\n    List(Vec<NoteBody>),\n}"
933            ),
934            "{code}"
935        );
936        assert!(code.contains("pub body: Option<NoteBodyValue>,"), "{code}");
937    }
938}