1use serde_json::Value;
19use std::collections::BTreeMap;
20use std::fmt::Write as _;
21
22pub struct Generation<'a> {
24 pub roots: &'a [&'a str],
26 pub elsewhere: Option<&'a str>,
29 pub identifiers: &'a [&'a str],
33}
34
35const EMPTY_OBJECT: &str = "EmptyObject";
36const STATED: &str = "fn stated";
38const INVALID_IDENTIFIER: &str = "InvalidIdentifier";
40
41pub 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 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 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 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 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 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 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 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 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 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 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 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
435struct Rule {
441 prefix: String,
442 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 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 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
534fn 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
556fn 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
588fn 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
614fn 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
623pub 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
642pub fn snake(word: &str) -> String {
644 let mut out = String::new();
645 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 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}