polywrap_schemafy_lib/
lib.rs

1// This would be nice once it stabilizes:
2// https://github.com/rust-lang/rust/issues/44732
3// #![feature(external_doc)]
4// #![doc(include = "../README.md")]
5
6//! This is a Rust crate which can take a [json schema (draft
7//! 4)](http://json-schema.org/) and generate Rust types which are
8//! serializable with [serde](https://serde.rs/). No checking such as
9//! `min_value` are done but instead only the structure of the schema
10//! is followed as closely as possible.
11//!
12//! As a schema could be arbitrarily complex this crate makes no
13//! guarantee that it can generate good types or even any types at all
14//! for a given schema but the crate does manage to bootstrap itself
15//! which is kind of cool.
16//!
17//! ## Example
18//!
19//! Generated types for VS Codes [debug server protocol][]: <https://docs.rs/debugserver-types>
20//!
21//! [debug server protocol]:https://code.visualstudio.com/docs/extensions/example-debuggers
22//!
23//! ## Usage
24//!
25//! Rust code is generated by providing a [`Schema`](./struct.Schema.html) struct (which can be deserialized from JSON).
26//!
27//! A proc macro is available in [`schemafy`](https://docs.rs/schemafy) crate
28//!
29//! ```rust
30//! extern crate serde;
31//! extern crate schemafy_core;
32//! extern crate serde_json;
33//!
34//! use serde::{Serialize, Deserialize};
35//! use schemafy_lib::Expander;
36//!
37//! let json = std::fs::read_to_string("src/schema.json").expect("Read schema JSON file");
38//!
39//! let schema = serde_json::from_str(&json).unwrap();
40//! let mut expander = Expander::new(
41//!     Some("Schema"),
42//!     "::schemafy_core::",
43//!     &schema,
44//! );
45//!
46//! let code = expander.expand(&schema);
47//! ```
48
49#[macro_use]
50extern crate serde_derive;
51
52#[macro_use]
53extern crate quote;
54
55pub mod generator;
56
57/// Types from the JSON Schema meta-schema (draft 4).
58///
59/// This module is itself generated from a JSON schema.
60mod schema;
61
62use std::{borrow::Cow, convert::TryFrom};
63
64use inflector::Inflector;
65
66use serde_json::Value;
67
68use uriparse::{Fragment, URI};
69
70pub use schema::{Schema, SimpleTypes};
71
72pub use generator::{Generator, GeneratorBuilder};
73
74use proc_macro2::{Span, TokenStream};
75
76fn replace_invalid_identifier_chars(s: &str) -> String {
77    s.strip_prefix('$')
78        .unwrap_or(s)
79        .replace(|c: char| !c.is_alphanumeric() && c != '_', "_")
80}
81
82fn replace_numeric_start(s: &str) -> String {
83    if s.chars().next().map(|c| c.is_numeric()).unwrap_or(false) {
84        format!("_{}", s)
85    } else {
86        s.to_string()
87    }
88}
89
90fn remove_excess_underscores(s: &str) -> String {
91    let mut result = String::new();
92    let mut char_iter = s.chars().peekable();
93
94    while let Some(c) = char_iter.next() {
95        let next_c = char_iter.peek();
96        if c != '_' || !matches!(next_c, Some('_')) {
97            result.push(c);
98        }
99    }
100
101    result
102}
103
104pub fn str_to_ident(s: &str) -> syn::Ident {
105    if s.is_empty() {
106        return syn::Ident::new("empty_", Span::call_site());
107    }
108
109    if s.chars().all(|c| c == '_') {
110        return syn::Ident::new("underscore_", Span::call_site());
111    }
112
113    let s = replace_invalid_identifier_chars(s);
114    let s = replace_numeric_start(&s);
115    let s = remove_excess_underscores(&s);
116
117    if s.is_empty() {
118        return syn::Ident::new("invalid_", Span::call_site());
119    }
120
121    let keywords = [
122        "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn",
123        "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref",
124        "return", "self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use",
125        "where", "while", "abstract", "become", "box", "do", "final", "macro", "override", "priv",
126        "typeof", "unsized", "virtual", "yield", "async", "await", "try",
127    ];
128    if keywords.iter().any(|&keyword| keyword == s) {
129        return syn::Ident::new(&format!("{}_", s), Span::call_site());
130    }
131
132    syn::Ident::new(&s, Span::call_site())
133}
134
135fn rename_keyword(prefix: &str, s: &str) -> Option<TokenStream> {
136    let n = str_to_ident(s);
137
138    if n == s {
139        return None;
140    }
141
142    if prefix.is_empty() {
143        Some(quote! {
144            #[serde(rename = #s)]
145            #n
146        })
147    } else {
148        let prefix = syn::Ident::new(prefix, Span::call_site());
149        Some(quote! {
150            #[serde(rename = #s)]
151            #prefix #n
152        })
153    }
154}
155
156fn field(s: &str) -> TokenStream {
157    if let Some(t) = rename_keyword("pub", s) {
158        return t;
159    }
160    let snake = s.to_snake_case();
161    if snake == s && !snake.contains(|c: char| c == '$' || c == '#') {
162        let field = syn::Ident::new(s, Span::call_site());
163        return quote!( pub #field );
164    }
165
166    let field = if snake.is_empty() {
167        syn::Ident::new("underscore", Span::call_site())
168    } else {
169        str_to_ident(&snake)
170    };
171
172    quote! {
173        #[serde(rename = #s)]
174        pub #field
175    }
176}
177
178fn merge_option<T, F>(mut result: &mut Option<T>, r: &Option<T>, f: F)
179where
180    F: FnOnce(&mut T, &T),
181    T: Clone,
182{
183    *result = match (&mut result, r) {
184        (&mut &mut Some(ref mut result), &Some(ref r)) => return f(result, r),
185        (&mut &mut None, &Some(ref r)) => Some(r.clone()),
186        _ => return,
187    };
188}
189
190fn merge_all_of(result: &mut Schema, r: &Schema) {
191    use std::collections::btree_map::Entry;
192
193    for (k, v) in &r.properties {
194        match result.properties.entry(k.clone()) {
195            Entry::Vacant(entry) => {
196                entry.insert(v.clone());
197            }
198            Entry::Occupied(mut entry) => merge_all_of(entry.get_mut(), v),
199        }
200    }
201
202    if let Some(ref ref_) = r.ref_ {
203        result.ref_ = Some(ref_.clone());
204    }
205
206    if let Some(ref description) = r.description {
207        result.description = Some(description.clone());
208    }
209
210    merge_option(&mut result.required, &r.required, |required, r_required| {
211        required.extend(r_required.iter().cloned());
212    });
213
214    result.type_.retain(|e| r.type_.contains(e));
215}
216
217const LINE_LENGTH: usize = 100;
218const INDENT_LENGTH: usize = 4;
219
220fn make_doc_comment(mut comment: &str, remaining_line: usize) -> TokenStream {
221    let mut out_comment = String::new();
222    out_comment.push_str("/// ");
223    let mut length = 4;
224    while let Some(word) = comment.split(char::is_whitespace).next() {
225        if comment.is_empty() {
226            break;
227        }
228        comment = &comment[word.len()..];
229        if length + word.len() >= remaining_line {
230            out_comment.push_str("\n/// ");
231            length = 4;
232        }
233        out_comment.push_str(word);
234        length += word.len();
235        let mut n = comment.chars();
236        match n.next() {
237            Some('\n') => {
238                out_comment.push('\n');
239                out_comment.push_str("/// ");
240                length = 4;
241            }
242            Some(_) => {
243                out_comment.push(' ');
244                length += 1;
245            }
246            None => (),
247        }
248        comment = n.as_str();
249    }
250    if out_comment.ends_with(' ') {
251        out_comment.pop();
252    }
253    out_comment.push('\n');
254    out_comment.parse().unwrap()
255}
256
257struct FieldExpander<'a, 'r: 'a> {
258    default: bool,
259    expander: &'a mut Expander<'r>,
260}
261
262impl<'a, 'r> FieldExpander<'a, 'r> {
263    fn expand_fields(&mut self, type_name: &str, schema: &Schema) -> Vec<TokenStream> {
264        let schema = self.expander.schema(schema);
265        schema
266            .properties
267            .iter()
268            .map(|(field_name, value)| {
269                self.expander.current_field.clone_from(field_name);
270                let key = field(field_name);
271                let required = schema
272                    .required
273                    .iter()
274                    .flat_map(|a| a.iter())
275                    .any(|req| req == field_name);
276                let field_type = self.expander.expand_type(type_name, required, value);
277                if !field_type.typ.starts_with("Option<") {
278                    self.default = false;
279                }
280                let typ = field_type.typ.parse::<TokenStream>().unwrap();
281
282                let default = if field_type.default {
283                    Some(quote! { #[serde(default)] })
284                } else {
285                    None
286                };
287                let attributes = if field_type.attributes.is_empty() {
288                    None
289                } else {
290                    let attributes = field_type
291                        .attributes
292                        .iter()
293                        .map(|attr| attr.parse::<TokenStream>().unwrap());
294                    Some(quote! {
295                        #[serde( #(#attributes),* )]
296                    })
297                };
298                let comment = value
299                    .description
300                    .as_ref()
301                    .map(|comment| make_doc_comment(comment, LINE_LENGTH - INDENT_LENGTH));
302                quote! {
303                    #comment
304                    #default
305                    #attributes
306                    #key : #typ
307                }
308            })
309            .collect()
310    }
311}
312
313pub struct Expander<'r> {
314    root_name: Option<&'r str>,
315    schemafy_path: &'r str,
316    root: &'r Schema,
317    current_type: String,
318    current_field: String,
319    types: Vec<(String, TokenStream)>,
320}
321
322struct FieldType {
323    typ: String,
324    attributes: Vec<String>,
325    default: bool,
326}
327
328impl<S> From<S> for FieldType
329where
330    S: Into<String>,
331{
332    fn from(s: S) -> FieldType {
333        FieldType {
334            typ: s.into(),
335            attributes: Vec::new(),
336            default: false,
337        }
338    }
339}
340
341impl<'r> Expander<'r> {
342    pub fn new(
343        root_name: Option<&'r str>,
344        schemafy_path: &'r str,
345        root: &'r Schema,
346    ) -> Expander<'r> {
347        Expander {
348            root_name,
349            root,
350            schemafy_path,
351            current_field: "".into(),
352            current_type: "".into(),
353            types: Vec::new(),
354        }
355    }
356
357    fn type_ref(&self, s: &str) -> String {
358        // ref is supposed to be be a valid URI, however we should better have a fallback plan
359        let fragment = URI::try_from(s)
360            .map(|uri| uri.fragment().map(Fragment::to_owned))
361            .ok()
362            .flatten()
363            .or({
364                let s = s.strip_prefix('#').unwrap_or(s);
365                Fragment::try_from(s).ok()
366            })
367            .map(|fragment| fragment.to_string())
368            .unwrap_or_else(|| s.to_owned());
369
370        let ref_ = if fragment.is_empty() {
371            self.root_name.expect("No root name specified for schema")
372        } else {
373            fragment.split('/').last().expect("Component")
374        };
375
376        let ref_ = ref_.to_pascal_case();
377        let ref_ = replace_invalid_identifier_chars(&ref_);
378        let ref_ = replace_numeric_start(&ref_);
379
380        format!("Box<{}>", ref_)
381    }
382
383    fn schema(&self, schema: &'r Schema) -> Cow<'r, Schema> {
384        let schema = match schema.ref_ {
385            Some(ref ref_) => self.schema_ref(ref_),
386            None => schema,
387        };
388        match schema.all_of {
389            Some(ref all_of) if !all_of.is_empty() => {
390                all_of
391                    .iter()
392                    .skip(1)
393                    .fold(self.schema(&all_of[0]).clone(), |mut result, def| {
394                        merge_all_of(result.to_mut(), &self.schema(def));
395                        result
396                    })
397            }
398            _ => Cow::Borrowed(schema),
399        }
400    }
401
402    fn schema_ref(&self, s: &str) -> &'r Schema {
403        s.split('/').fold(self.root, |schema, comp| {
404            if comp.ends_with('#') {
405                self.root
406            } else if comp == "definitions" {
407                schema
408            } else {
409                schema
410                    .definitions
411                    .get(comp)
412                    .unwrap_or_else(|| panic!("Expected definition: `{}` {}", s, comp))
413            }
414        })
415    }
416
417    fn expand_type(&mut self, type_name: &str, required: bool, typ: &Schema) -> FieldType {
418        let saved_type = self.current_type.clone();
419        let mut result = self.expand_type_(typ);
420        self.current_type = saved_type;
421        if type_name.to_pascal_case() == result.typ.to_pascal_case() {
422            result.typ = format!("Box<{}>", result.typ)
423        }
424        if !required {
425            if !result.default {
426                result.typ = format!("Option<{}>", result.typ);
427            }
428            if result.typ.starts_with("Option<") {
429                result
430                    .attributes
431                    .push("skip_serializing_if=\"Option::is_none\"".into());
432            }
433        }
434        result
435    }
436
437    fn expand_type_(&mut self, typ: &Schema) -> FieldType {
438        if let Some(ref ref_) = typ.ref_ {
439            self.type_ref(ref_).into()
440        } else if typ.any_of.as_ref().map_or(false, |a| a.len() >= 2) {
441            let any_of = typ.any_of.as_ref().unwrap();
442            let simple = self.schema(&any_of[0]);
443            let array = self.schema(&any_of[1]);
444            if !array.type_.is_empty() {
445                if let SimpleTypes::Array = array.type_[0] {
446                    if simple == self.schema(&array.items[0]) {
447                        return FieldType {
448                            typ: format!("Vec<{}>", self.expand_type_(&any_of[0]).typ),
449                            attributes: vec![format!(
450                                r#"with="{}one_or_many""#,
451                                self.schemafy_path
452                            )],
453                            default: true,
454                        };
455                    }
456                }
457            }
458            "serde_json::Value".into()
459        } else if typ.one_of.as_ref().map_or(false, |a| a.len() >= 2) {
460            let schemas = typ.one_of.as_ref().unwrap();
461            let (type_name, type_def) = self.expand_one_of(schemas);
462            self.types.push((type_name.clone(), type_def));
463            type_name.into()
464        } else if typ.type_.len() == 2 {
465            if typ.type_[0] == SimpleTypes::Null || typ.type_[1] == SimpleTypes::Null {
466                let mut ty = typ.clone();
467                ty.type_.retain(|x| *x != SimpleTypes::Null);
468
469                FieldType {
470                    typ: format!("Option<{}>", self.expand_type_(&ty).typ),
471                    attributes: vec![],
472                    default: true,
473                }
474            } else {
475                "serde_json::Value".into()
476            }
477        } else if typ.type_.len() == 1 {
478            match typ.type_[0] {
479                SimpleTypes::String => {
480                    if typ.enum_.as_ref().map_or(false, |e| e.is_empty()) {
481                        "serde_json::Value".into()
482                    } else {
483                        "String".into()
484                    }
485                }
486                SimpleTypes::Integer => "i64".into(),
487                SimpleTypes::Boolean => "bool".into(),
488                SimpleTypes::Number => "f64".into(),
489                // Handle objects defined inline
490                SimpleTypes::Object
491                    if !typ.properties.is_empty()
492                        || typ.additional_properties == Some(Value::Bool(false)) =>
493                {
494                    let name = format!(
495                        "{}{}",
496                        self.current_type.to_pascal_case(),
497                        self.current_field.to_pascal_case()
498                    );
499                    let tokens = self.expand_schema(&name, typ);
500                    self.types.push((name.clone(), tokens));
501                    name.into()
502                }
503                SimpleTypes::Object => {
504                    let prop = match typ.additional_properties {
505                        Some(ref props) if props.is_object() => {
506                            let prop = serde_json::from_value(props.clone()).unwrap();
507                            self.expand_type_(&prop).typ
508                        }
509                        _ => "serde_json::Value".into(),
510                    };
511                    let result = format!("::std::collections::BTreeMap<String, {}>", prop);
512                    FieldType {
513                        typ: result,
514                        attributes: Vec::new(),
515                        default: typ.default == Some(Value::Object(Default::default())),
516                    }
517                }
518                SimpleTypes::Array => {
519                    let item_type = typ.items.get(0).map_or("serde_json::Value".into(), |item| {
520                        self.current_type = format!("{}Item", self.current_type);
521                        self.expand_type_(item).typ
522                    });
523                    format!("Vec<{}>", item_type).into()
524                }
525                _ => "serde_json::Value".into(),
526            }
527        } else {
528            "serde_json::Value".into()
529        }
530    }
531
532    fn expand_one_of(&mut self, schemas: &[Schema]) -> (String, TokenStream) {
533        let current_field = if self.current_field.is_empty() {
534            "".to_owned()
535        } else {
536            str_to_ident(&self.current_field)
537                .to_string()
538                .to_pascal_case()
539        };
540        let saved_type = format!("{}{}", self.current_type, current_field);
541        if schemas.is_empty() {
542            return (saved_type, TokenStream::new());
543        }
544        let (variant_names, variant_types): (Vec<_>, Vec<_>) = schemas
545            .iter()
546            .enumerate()
547            .map(|(i, schema)| {
548                let name = schema.id.clone().unwrap_or_else(|| format!("Variant{}", i));
549                if let Some(ref_) = &schema.ref_ {
550                    let type_ = self.type_ref(ref_);
551                    (format_ident!("{}", &name), format_ident!("{}", &type_))
552                } else {
553                    let type_name = format!("{}{}", saved_type, &name);
554                    let field_type = self.expand_schema(&type_name, schema);
555                    self.types.push((type_name.clone(), field_type));
556                    (format_ident!("{}", &name), format_ident!("{}", &type_name))
557                }
558            })
559            .unzip();
560        let type_name_ident = syn::Ident::new(&saved_type, Span::call_site());
561        let type_def = quote! {
562            #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
563            #[serde(untagged)]
564            pub enum #type_name_ident {
565                #(#variant_names(#variant_types)),*
566            }
567        };
568        (saved_type, type_def)
569    }
570
571    fn expand_definitions(&mut self, schema: &Schema) {
572        for (name, def) in &schema.definitions {
573            let type_decl = self.expand_schema(name, def);
574            let definition_tokens = match def.description {
575                Some(ref comment) => {
576                    let t = make_doc_comment(comment, LINE_LENGTH);
577                    quote! {
578                        #t
579                        #type_decl
580                    }
581                }
582                None => type_decl,
583            };
584            self.types.push((name.to_string(), definition_tokens));
585        }
586    }
587
588    fn expand_schema(&mut self, original_name: &str, schema: &Schema) -> TokenStream {
589        self.expand_definitions(schema);
590
591        let pascal_case_name = replace_invalid_identifier_chars(&original_name.to_pascal_case());
592        self.current_type.clone_from(&pascal_case_name);
593        let (fields, default) = {
594            let mut field_expander = FieldExpander {
595                default: true,
596                expander: self,
597            };
598            let fields = field_expander.expand_fields(original_name, schema);
599            (fields, field_expander.default)
600        };
601        let name = syn::Ident::new(&pascal_case_name, Span::call_site());
602        let is_struct =
603            !fields.is_empty() || schema.additional_properties == Some(Value::Bool(false));
604        let serde_rename = if name == original_name {
605            None
606        } else {
607            Some(quote! {
608                #[serde(rename = #original_name)]
609            })
610        };
611        let is_enum = schema.enum_.as_ref().map_or(false, |e| !e.is_empty());
612        let type_decl = if is_struct {
613            let serde_deny_unknown = if schema.additional_properties == Some(Value::Bool(false))
614                && schema.pattern_properties.is_empty()
615            {
616                Some(quote! { #[serde(deny_unknown_fields)] })
617            } else {
618                None
619            };
620            if default {
621                quote! {
622                    #[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize)]
623                    #serde_rename
624                    #serde_deny_unknown
625                    pub struct #name {
626                        #(#fields),*
627                    }
628                }
629            } else {
630                quote! {
631                    #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
632                    #serde_rename
633                    #serde_deny_unknown
634                    pub struct #name {
635                        #(#fields),*
636                    }
637                }
638            }
639        } else if is_enum {
640            let mut optional = false;
641            let mut repr_i64 = false;
642            let variants = if schema.enum_names.as_ref().map_or(false, |e| !e.is_empty()) {
643                let values = schema.enum_.as_ref().map_or(&[][..], |v| v);
644                let names = schema.enum_names.as_ref().map_or(&[][..], |v| v);
645                if names.len() != values.len() {
646                    panic!(
647                        "enumNames(length {}) and enum(length {}) have different length",
648                        names.len(),
649                        values.len()
650                    )
651                }
652                names
653                    .iter()
654                    .enumerate()
655                    .map(|(idx, name)| (&values[idx], name))
656                    .flat_map(|(value, name)| {
657                        let pascal_case_variant = name.to_pascal_case();
658                        let variant_name =
659                            rename_keyword("", &pascal_case_variant).unwrap_or_else(|| {
660                                let v = syn::Ident::new(&pascal_case_variant, Span::call_site());
661                                quote!(#v)
662                            });
663                        match value {
664                            Value::String(ref s) => Some(quote! {
665                                #[serde(rename = #s)]
666                                #variant_name
667                            }),
668                            Value::Number(ref n) => {
669                                repr_i64 = true;
670                                let num = syn::LitInt::new(&n.to_string(), Span::call_site());
671                                Some(quote! {
672                                    #variant_name = #num
673                                })
674                            }
675                            Value::Null => {
676                                optional = true;
677                                None
678                            }
679                            _ => panic!("Expected string,bool or number for enum got `{}`", value),
680                        }
681                    })
682                    .collect::<Vec<_>>()
683            } else {
684                schema
685                    .enum_
686                    .as_ref()
687                    .map_or(&[][..], |v| v)
688                    .iter()
689                    .flat_map(|v| match *v {
690                        Value::String(ref v) => {
691                            let pascal_case_variant = v.to_pascal_case();
692                            let variant_name = rename_keyword("", &pascal_case_variant)
693                                .unwrap_or_else(|| {
694                                    let v =
695                                        syn::Ident::new(&pascal_case_variant, Span::call_site());
696                                    quote!(#v)
697                                });
698                            Some(if pascal_case_variant == *v {
699                                variant_name
700                            } else {
701                                quote! {
702                                    #[serde(rename = #v)]
703                                    #variant_name
704                                }
705                            })
706                        }
707                        Value::Null => {
708                            optional = true;
709                            None
710                        }
711                        _ => panic!("Expected string for enum got `{}`", v),
712                    })
713                    .collect::<Vec<_>>()
714            };
715            if optional {
716                let enum_name = syn::Ident::new(&format!("{}_", name), Span::call_site());
717                if repr_i64 {
718                    quote! {
719                        pub type #name = Option<#enum_name>;
720                        #[derive(Clone, PartialEq, Debug, Serialize_repr, Deserialize_repr)]
721                        #serde_rename
722                        #[repr(i64)]
723                        pub enum #enum_name {
724                            #(#variants),*
725                        }
726                    }
727                } else {
728                    quote! {
729                        pub type #name = Option<#enum_name>;
730                        #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
731                        #serde_rename
732                        pub enum #enum_name {
733                            #(#variants),*
734                        }
735                    }
736                }
737            } else if repr_i64 {
738                quote! {
739                    #[derive(Clone, PartialEq, Debug, Serialize_repr, Deserialize_repr)]
740                    #serde_rename
741                    #[repr(i64)]
742                    pub enum #name {
743                        #(#variants),*
744                    }
745                }
746            } else {
747                quote! {
748                    #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
749                    #serde_rename
750                    pub enum #name {
751                        #(#variants),*
752                    }
753                }
754            }
755        } else {
756            let typ = self
757                .expand_type("", true, schema)
758                .typ
759                .parse::<TokenStream>()
760                .unwrap();
761            // Skip self-referential types, e.g. `struct Schema = Schema`
762            if name == typ.to_string() {
763                return TokenStream::new();
764            }
765            return quote! {
766                pub type #name = #typ;
767            };
768        };
769        type_decl
770    }
771
772    pub fn expand(&mut self, schema: &Schema) -> TokenStream {
773        match self.root_name {
774            Some(name) => {
775                let schema = self.expand_schema(name, schema);
776                self.types.push((name.to_string(), schema));
777            }
778            None => self.expand_definitions(schema),
779        }
780
781        let types = self.types.iter().map(|t| &t.1);
782
783        quote! {
784            #( #types )*
785        }
786    }
787
788    pub fn expand_root(&mut self) -> TokenStream {
789        self.expand(self.root)
790    }
791}
792
793#[cfg(test)]
794mod tests {
795    use super::*;
796
797    #[test]
798    fn test_expander_type_ref() {
799        let json = std::fs::read_to_string("src/schema.json").expect("Read schema JSON file");
800        let schema = serde_json::from_str(&json).unwrap_or_else(|err| panic!("{}", err));
801        let expander = Expander::new(Some("SchemaName"), "::schemafy_core::", &schema);
802
803        assert_eq!(expander.type_ref("normalField"), "NormalField");
804        assert_eq!(expander.type_ref("#"), "SchemaName");
805        assert_eq!(expander.type_ref(""), "SchemaName");
806        assert_eq!(expander.type_ref("1"), "_1");
807        assert_eq!(
808            expander.type_ref("http://example.com/schema.json#"),
809            "SchemaName"
810        );
811        assert_eq!(
812            expander.type_ref("http://example.com/normalField#withFragment"),
813            "WithFragment"
814        );
815        assert_eq!(
816            expander.type_ref("http://example.com/normalField#withFragment/and/path"),
817            "Path"
818        );
819        assert_eq!(
820            expander.type_ref("http://example.com/normalField?with&params#andFragment/and/path"),
821            "Path"
822        );
823        assert_eq!(expander.type_ref("#/only/Fragment"), "Fragment");
824
825        // Invalid cases, just to verify the behavior
826        assert_eq!(expander.type_ref("ref"), "Ref");
827        assert_eq!(expander.type_ref("_"), "");
828        assert_eq!(expander.type_ref("thieves' tools"), "ThievesTools");
829        assert_eq!(
830            expander.type_ref("http://example.com/normalField?with&params=1"),
831            "NormalFieldWithParams1"
832        );
833    }
834
835    #[test]
836    fn embedded_type_names() {
837        use std::collections::HashSet;
838
839        let json = std::fs::read_to_string("tests/multiple-property-types.json")
840            .expect("Read schema JSON file");
841        let schema = serde_json::from_str(&json).unwrap();
842        let mut expander = Expander::new(Some("Root"), "UNUSED", &schema);
843        expander.expand(&schema);
844
845        // check that the type names for embedded objects only include their
846        // ancestors' type names, and not names from unrelated fields
847        let types = expander
848            .types
849            .iter()
850            .map(|v| v.0.as_str())
851            .collect::<HashSet<&str>>();
852        assert!(types.contains("RootItemAC"));
853        assert!(types.contains("RootKM"));
854        assert!(types.contains("RootTV"));
855    }
856}