Skip to main content

typify_impl/
util.rs

1// Copyright 2025 Oxide Computer Company
2
3use std::collections::{BTreeMap, BTreeSet, HashSet};
4
5use heck::ToPascalCase;
6use log::debug;
7use schemars::schema::{
8    ArrayValidation, InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SingleOrVec,
9    StringValidation, SubschemaValidation,
10};
11use unicode_ident::{is_xid_continue, is_xid_start};
12
13use crate::{validate::schema_value_validate, Error, Name, RefKey, Result, TypeSpace};
14
15pub(crate) fn metadata_description(metadata: &Option<Box<Metadata>>) -> Option<String> {
16    metadata
17        .as_ref()
18        .and_then(|metadata| metadata.description.as_ref().cloned())
19}
20
21pub(crate) fn metadata_title(metadata: &Option<Box<Metadata>>) -> Option<String> {
22    metadata
23        .as_ref()
24        .and_then(|metadata| metadata.title.as_ref().cloned())
25}
26
27pub(crate) fn metadata_title_and_description(metadata: &Option<Box<Metadata>>) -> Option<String> {
28    metadata
29        .as_ref()
30        .and_then(|metadata| match (&metadata.title, &metadata.description) {
31            (Some(t), Some(d)) => Some(format!("{}\n\n{}", t, d)),
32            (Some(t), None) => Some(t.clone()),
33            (None, Some(d)) => Some(d.clone()),
34            (None, None) => None,
35        })
36}
37
38/// Check if all schemas are mutually exclusive.
39///
40/// TODO This is used to turn an `anyOf` into a `oneOf` (i.e. if true). However
41/// a better approach is probably to use the (newish) merge logic to try to
42/// explode an `anyOf` into its 2^N possibilities. Some of those may be invalid
43/// in which case we'll end up looking like a `oneOf`. The logic of merging is
44/// conceptually identical to the logic below that validates **if** the schemas
45/// **could** be merged (i.e. if they're compatible).
46pub(crate) fn all_mutually_exclusive(
47    subschemas: &[Schema],
48    definitions: &BTreeMap<RefKey, Schema>,
49) -> bool {
50    let len = subschemas.len();
51    // With fewer than two subschemas, this is a degenerate case where the
52    // lone schema is mutually exclusive with everything else (which happens
53    // to be nothing).
54    if len < 2 {
55        return true;
56    }
57    // Consider all pairs
58    (0..len - 1)
59        .flat_map(|ii| (ii + 1..len).map(move |jj| (ii, jj)))
60        .all(|(ii, jj)| {
61            let a = resolve(&subschemas[ii], definitions);
62            let b = resolve(&subschemas[jj], definitions);
63            schemas_mutually_exclusive(a, b, definitions)
64        })
65}
66
67/// This function needs to necessarily be conservative. We'd much prefer a
68/// false negative than a false positive.
69fn schemas_mutually_exclusive(
70    a: &Schema,
71    b: &Schema,
72    definitions: &BTreeMap<RefKey, Schema>,
73) -> bool {
74    match (a, b) {
75        // If either matches nothing then they are exclusive.
76        (Schema::Bool(false), _) => true,
77        (_, Schema::Bool(false)) => true,
78
79        // If either matches anything then they are not exclusive.
80        (Schema::Bool(true), _) => false,
81        (_, Schema::Bool(true)) => false,
82
83        // Iterate over subschemas.
84        (
85            other,
86            Schema::Object(SchemaObject {
87                metadata: None,
88                instance_type: None,
89                format: None,
90                enum_values: None,
91                const_value: None,
92                subschemas: Some(subschemas),
93                number: None,
94                string: None,
95                array: None,
96                object: None,
97                reference: None,
98                extensions: _,
99            }),
100        )
101        | (
102            Schema::Object(SchemaObject {
103                metadata: None,
104                instance_type: None,
105                format: None,
106                enum_values: None,
107                const_value: None,
108                subschemas: Some(subschemas),
109                number: None,
110                string: None,
111                array: None,
112                object: None,
113                reference: None,
114                extensions: _,
115            }),
116            other,
117        ) => match subschemas.as_ref() {
118            // For an allOf, *any* subschema incompatibility means that the
119            // schemas are mutually exclusive.
120            SubschemaValidation {
121                all_of: Some(s),
122                any_of: None,
123                one_of: None,
124                not: None,
125                if_schema: None,
126                then_schema: None,
127                else_schema: None,
128            } => s
129                .iter()
130                .any(|sub| schemas_mutually_exclusive(sub, other, definitions)),
131
132            // For a oneOf or anyOf, *all* subschemas need to be incompatible.
133            SubschemaValidation {
134                all_of: None,
135                any_of: Some(s),
136                one_of: None,
137                not: None,
138                if_schema: None,
139                then_schema: None,
140                else_schema: None,
141            }
142            | SubschemaValidation {
143                all_of: None,
144                any_of: None,
145                one_of: Some(s),
146                not: None,
147                if_schema: None,
148                then_schema: None,
149                else_schema: None,
150            } => s
151                .iter()
152                .all(|sub| schemas_mutually_exclusive(sub, other, definitions)),
153
154            // For a not, they're mutually exclusive if they *do* match.
155            SubschemaValidation {
156                all_of: None,
157                any_of: None,
158                one_of: None,
159                not: Some(sub),
160                if_schema: None,
161                then_schema: None,
162                else_schema: None,
163            } => !schemas_mutually_exclusive(sub, other, definitions),
164
165            // Assume other subschemas are complex to understand and may
166            // therefore be compatible.
167            _ => false,
168        },
169
170        // If one schema has enumerated values, it's incompatible if all values
171        // fail to validate against the other schema. (Conversely, if all
172        // values *do* validate against the other schema, it simply seems
173        // redundant--the most interesting case is if *some* values validate
174        // and others do not.)
175        (
176            schema,
177            Schema::Object(SchemaObject {
178                instance_type: None,
179                enum_values: Some(enum_values),
180                ..
181            }),
182        )
183        | (
184            Schema::Object(SchemaObject {
185                instance_type: None,
186                enum_values: Some(enum_values),
187                ..
188            }),
189            schema,
190        ) => enum_values
191            .iter()
192            .all(|value| schema_value_validate(schema, value, definitions).is_err()),
193
194        // If one schema has a constant value, it's incompatible if that value
195        // fails to validate against the other schema.
196        (
197            schema,
198            Schema::Object(SchemaObject {
199                const_value: Some(value),
200                ..
201            }),
202        )
203        | (
204            Schema::Object(SchemaObject {
205                const_value: Some(value),
206                ..
207            }),
208            schema,
209        ) => schema_value_validate(schema, value, definitions).is_err(),
210
211        // Neither is a Schema::Bool; we need to look at the instance types.
212        (Schema::Object(a), Schema::Object(b)) => {
213            match (&a.instance_type, &b.instance_type) {
214                // If either is None, assume we're dealing with a more complex
215                // type and that they are not exclusive.
216                (None, _) => false,
217                (_, None) => false,
218
219                // If each schema has a single type and they aren't the same
220                // then the types must be mutually exclusive.
221                (Some(SingleOrVec::Single(a_single)), Some(SingleOrVec::Single(b_single)))
222                    if a_single != b_single =>
223                {
224                    true
225                }
226
227                // For two objects we need to check required properties and
228                // additional properties to see if there exists an object that
229                // could successfully be validated by either schema.
230                (Some(SingleOrVec::Single(a_single)), Some(SingleOrVec::Single(b_single)))
231                    if a_single == b_single && a_single.as_ref() == &InstanceType::Object =>
232                {
233                    if let (
234                        SchemaObject {
235                            metadata: _,
236                            instance_type: _,
237                            format: None,
238                            enum_values: None,
239                            const_value: None,
240                            subschemas: None,
241                            number: None,
242                            string: None,
243                            array: None,
244                            object: Some(a_validation),
245                            reference: None,
246                            extensions: _,
247                        },
248                        SchemaObject {
249                            metadata: _,
250                            instance_type: _,
251                            format: None,
252                            enum_values: None,
253                            const_value: None,
254                            subschemas: None,
255                            number: None,
256                            string: None,
257                            array: None,
258                            object: Some(b_validation),
259                            reference: None,
260                            extensions: _,
261                        },
262                    ) = (a, b)
263                    {
264                        object_schemas_mutually_exclusive(a_validation, b_validation)
265                    } else {
266                        // Could check further, but we'll be conservative.
267                        false
268                    }
269                }
270
271                // For two objects we need to check required properties and
272                // additional properties to see if there exists an object that
273                // could successfully be validated by either schema.
274                (Some(SingleOrVec::Single(a_single)), Some(SingleOrVec::Single(b_single)))
275                    if a_single == b_single && a_single.as_ref() == &InstanceType::Array =>
276                {
277                    if let (
278                        SchemaObject {
279                            metadata: _,
280                            instance_type: _,
281                            format: None,
282                            enum_values: None,
283                            const_value: None,
284                            subschemas: None,
285                            number: None,
286                            string: None,
287                            array: Some(a_validation),
288                            object: None,
289                            reference: None,
290                            extensions: _,
291                        },
292                        SchemaObject {
293                            metadata: _,
294                            instance_type: _,
295                            format: None,
296                            enum_values: None,
297                            const_value: None,
298                            subschemas: None,
299                            number: None,
300                            string: None,
301                            array: Some(b_validation),
302                            object: None,
303                            reference: None,
304                            extensions: _,
305                        },
306                    ) = (a, b)
307                    {
308                        array_schemas_mutually_exclusive(a_validation, b_validation, definitions)
309                    } else {
310                        // Could check further, but we'll be conservative.
311                        false
312                    }
313                }
314
315                // For other simple types, check if the single type is the same
316                // or not.
317                (Some(SingleOrVec::Single(a_single)), Some(SingleOrVec::Single(b_single))) => {
318                    a_single != b_single
319                }
320
321                // For two schemas with lists of instance types, make sure that
322                // all pairs differ.
323                (Some(SingleOrVec::Vec(a_vec)), Some(SingleOrVec::Vec(b_vec))) => a_vec
324                    .iter()
325                    .all(|instance_type| !b_vec.contains(instance_type)),
326
327                // If one is a single type and the other is a vec, it will
328                // suffice for now to check that the single item is different
329                // than all the items in the vec.
330                (Some(SingleOrVec::Single(single)), Some(SingleOrVec::Vec(vec)))
331                | (Some(SingleOrVec::Vec(vec)), Some(SingleOrVec::Single(single))) => {
332                    !vec.contains(single)
333                }
334            }
335        }
336    }
337}
338
339// See if there are unique, required properties of each that cannot be present
340// in the other. In other words, see if there are properties that would
341// uniquely identify an objects as validating exclusively with one or the other
342// (but not with both).
343fn object_schemas_mutually_exclusive(
344    a_validation: &ObjectValidation,
345    b_validation: &ObjectValidation,
346) -> bool {
347    let ObjectValidation {
348        required: a_required,
349        properties: a_properties,
350        ..
351    } = a_validation;
352    let ObjectValidation {
353        required: b_required,
354        properties: b_properties,
355        ..
356    } = b_validation;
357
358    // No properties? Too permissive / insufficiently exclusive.
359    if a_properties.is_empty() || b_properties.is_empty() {
360        return false;
361    }
362
363    // Either set of required properties must not be a subset of the other's
364    // properties i.e. if there's a property that *must* be in one of the two
365    // objects, and *cannot* be in the other, a property whose presence or
366    // absence determines which of the two objects is relevant.
367    if !a_required.is_subset(&b_properties.keys().cloned().collect())
368        || !b_required.is_subset(&a_properties.keys().cloned().collect())
369    {
370        true
371    } else {
372        // Even if all required properties of each is a permitted property of
373        // the other, each may have required properties that have fixed values
374        // that differ. This can happen in particular for internally or
375        // adjacently tagged enums where the properties may be identical but
376        // the value of the tag property will be unique.
377
378        // Compute the set that consists of fixed-value properties--a
379        // tuple of the property name and the fixed value. Note that we may
380        // encounter objects that specify that a field is required, but do
381        // *not* specify the field. That's ok, but we can't assure mutual
382        // exclusivity.
383        let aa = a_required
384            .iter()
385            .filter_map(|name| {
386                let t = a_properties.get(name).unwrap();
387                constant_string_value(t).map(|s| (name.clone(), s))
388            })
389            .collect::<HashSet<_>>();
390        let bb = b_required
391            .iter()
392            .filter_map(|name| {
393                let t = b_properties.get(name).unwrap();
394                constant_string_value(t).map(|s| (name.clone(), s))
395            })
396            .collect::<HashSet<_>>();
397
398        // True if neither is a subset of the other.
399        !aa.is_subset(&bb) && !bb.is_subset(&aa)
400    }
401}
402
403fn array_schemas_mutually_exclusive(
404    a_validation: &ArrayValidation,
405    b_validation: &ArrayValidation,
406    definitions: &BTreeMap<RefKey, Schema>,
407) -> bool {
408    match (a_validation, b_validation) {
409        // If one is an array with a single item type and the other is a tuple
410        // of a fixed size with fixed item types, we could only see a conflict
411        // if the single item was compatible with *all* types of the tuple.
412        // It's therefore sufficient to see if it's exclusive with *any* of the
413        // types of the tuple.
414        (
415            ArrayValidation {
416                items: Some(SingleOrVec::Single(single)),
417                additional_items: None,
418                ..
419            },
420            ArrayValidation {
421                items: Some(SingleOrVec::Vec(vec)),
422                additional_items: None,
423                max_items: Some(max_items),
424                min_items: Some(min_items),
425                unique_items: None,
426                contains: None,
427            },
428        )
429        | (
430            ArrayValidation {
431                items: Some(SingleOrVec::Vec(vec)),
432                additional_items: None,
433                max_items: Some(max_items),
434                min_items: Some(min_items),
435                unique_items: None,
436                contains: None,
437            },
438            ArrayValidation {
439                items: Some(SingleOrVec::Single(single)),
440                additional_items: None,
441                ..
442            },
443        ) if max_items == min_items && *max_items as usize == vec.len() => vec
444            .iter()
445            .any(|schema| schemas_mutually_exclusive(schema, single, definitions)),
446
447        (aa, bb) => {
448            // If min > max then these schemas are incompatible.
449            match (&aa.max_items, &bb.min_items) {
450                (Some(max), Some(min)) if min > max => return true,
451                _ => (),
452            }
453            match (&bb.max_items, &aa.min_items) {
454                (Some(max), Some(min)) if min > max => return true,
455                _ => (),
456            }
457
458            match (&aa.items, &aa.max_items, &bb.items, &bb.max_items) {
459                // If thee's a single item schema and it's mutually exclusive
460                // then we're done.
461                (Some(SingleOrVec::Single(a_items)), _, Some(SingleOrVec::Single(b_items)), _)
462                    if schemas_mutually_exclusive(a_items, b_items, definitions) =>
463                {
464                    return true;
465                }
466
467                _ => (),
468            }
469            debug!(
470                "giving up on mutual exclusivity check {} {}",
471                serde_json::to_string_pretty(aa).unwrap(),
472                serde_json::to_string_pretty(bb).unwrap(),
473            );
474            false
475        }
476    }
477}
478
479/// If this schema represents a constant-value string, return that string,
480/// otherwise return None.
481pub(crate) fn constant_string_value(schema: &Schema) -> Option<&str> {
482    match schema {
483        // Singleton, typed enumerated value.
484        Schema::Object(SchemaObject {
485            metadata: _,
486            instance_type: Some(SingleOrVec::Single(single)),
487            format: None,
488            enum_values: Some(values),
489            const_value: None,
490            subschemas: None,
491            number: None,
492            string: None,
493            array: None,
494            object: None,
495            reference: None,
496            extensions: _,
497        }) if single.as_ref() == &InstanceType::String && values.len() == 1 => {
498            values.first().unwrap().as_str()
499        }
500
501        // Singleton, untyped enumerated value.
502        Schema::Object(SchemaObject {
503            metadata: _,
504            instance_type: None,
505            format: None,
506            enum_values: Some(values),
507            const_value: None,
508            subschemas: None,
509            number: None,
510            string: None,
511            array: None,
512            object: None,
513            reference: None,
514            extensions: _,
515        }) if values.len() == 1 => values.first().unwrap().as_str(),
516
517        // Constant value.
518        Schema::Object(SchemaObject {
519            metadata: _,
520            instance_type: Some(SingleOrVec::Single(single)),
521            format: None,
522            enum_values: None,
523            const_value: Some(value),
524            subschemas: None,
525            number: None,
526            string: None,
527            array: None,
528            object: None,
529            reference: None,
530            extensions: _,
531        }) if single.as_ref() == &InstanceType::String => value.as_str(),
532
533        // Constant, untyped value.
534        Schema::Object(SchemaObject {
535            metadata: _,
536            instance_type: None,
537            format: None,
538            enum_values: None,
539            const_value: Some(value),
540            subschemas: None,
541            number: None,
542            string: None,
543            array: None,
544            object: None,
545            reference: None,
546            extensions: _,
547        }) => value.as_str(),
548
549        _ => None,
550    }
551}
552
553fn decode_segment(segment: &str) -> String {
554    segment.replace("~1", "/").replace("~0", "~")
555}
556
557pub(crate) fn ref_key(ref_name: &str) -> RefKey {
558    if ref_name == "#" {
559        RefKey::Root
560    } else if let Some(idx) = ref_name.rfind('/') {
561        let decoded_segment = decode_segment(&ref_name[idx + 1..]);
562
563        RefKey::Def(decoded_segment)
564    } else {
565        panic!("expected a '/' in $ref: {}", ref_name)
566    }
567}
568
569fn resolve<'a>(
570    schema: &'a Schema,
571    definitions: &'a std::collections::BTreeMap<RefKey, Schema>,
572) -> &'a Schema {
573    match schema {
574        Schema::Bool(_) => schema,
575        Schema::Object(SchemaObject {
576            metadata: _,
577            instance_type: None,
578            format: None,
579            enum_values: None,
580            const_value: None,
581            subschemas: None,
582            number: None,
583            string: None,
584            array: None,
585            object: None,
586            reference: Some(ref_name),
587            extensions: _,
588        }) => definitions.get(&ref_key(ref_name)).unwrap(),
589        Schema::Object(SchemaObject {
590            reference: None, ..
591        }) => schema,
592        // TODO Not sure what this would mean...
593        _ => todo!(),
594    }
595}
596
597/// Determine if a schema has a name (potentially).
598pub(crate) fn schema_is_named(schema: &Schema) -> Option<String> {
599    let raw_name = match schema {
600        Schema::Object(SchemaObject {
601            metadata: _,
602            instance_type: None,
603            format: None,
604            enum_values: None,
605            const_value: None,
606            subschemas: None,
607            number: None,
608            string: None,
609            array: None,
610            object: None,
611            reference: Some(reference),
612            extensions: _,
613        }) => {
614            let idx = reference.rfind('/')?;
615            Some(reference[idx + 1..].to_string())
616        }
617
618        Schema::Object(SchemaObject {
619            metadata: Some(metadata),
620            ..
621        }) if metadata.as_ref().title.is_some() => Some(metadata.as_ref().title.as_ref()?.clone()),
622
623        Schema::Object(SchemaObject {
624            metadata: _,
625            instance_type: _,
626            format: None,
627            enum_values: None,
628            const_value: None,
629            subschemas: Some(subschemas),
630            number: None,
631            string: None,
632            array: None,
633            object: None,
634            reference: None,
635            extensions: _,
636        }) => singleton_subschema(subschemas).and_then(schema_is_named),
637
638        // Best-effort fallback for things with raw types that can be easily inferred
639        Schema::Object(SchemaObject {
640            instance_type: Some(SingleOrVec::Single(single)),
641            format,
642            ..
643        }) => match (**single, format.as_deref()) {
644            (_, Some(format)) => Some(format.to_pascal_case()),
645            (InstanceType::Boolean, _) => Some("Boolean".to_string()),
646            (InstanceType::Integer, _) => Some("Integer".to_string()),
647            (InstanceType::Number, _) => Some("Number".to_string()),
648            (InstanceType::String, _) => Some("String".to_string()),
649            (InstanceType::Array, _) => Some("Array".to_string()),
650            (InstanceType::Object, _) => Some("Object".to_string()),
651            (InstanceType::Null, _) => Some("Null".to_string()),
652        },
653
654        _ => None,
655    }?;
656
657    Some(sanitize(&raw_name, Case::Pascal))
658}
659
660/// Return the object data or None if it's not an object (or doesn't conform to
661/// the objects we know how to handle).
662pub(crate) fn get_object(schema: &Schema) -> Option<(&Option<Box<Metadata>>, &ObjectValidation)> {
663    match schema {
664        // Object
665        Schema::Object(SchemaObject {
666            metadata,
667            instance_type: Some(SingleOrVec::Single(single)),
668            format: None,
669            enum_values: None,
670            const_value: None,
671            subschemas: None,
672            number: _,
673            string: _,
674            array: _,
675            object: Some(validation),
676            reference: None,
677            extensions: _,
678        }) if single.as_ref() == &InstanceType::Object
679            && schema_none_or_false(&validation.additional_properties)
680            && validation.max_properties.is_none()
681            && validation.min_properties.is_none()
682            && validation.pattern_properties.is_empty()
683            && validation.property_names.is_none() =>
684        {
685            Some((metadata, validation.as_ref()))
686        }
687        // Object with no explicit type (but the proper validation)
688        Schema::Object(SchemaObject {
689            metadata,
690            instance_type: None,
691            format: None,
692            enum_values: None,
693            const_value: None,
694            subschemas: None,
695            number: None,
696            string: None,
697            array: None,
698            object: Some(validation),
699            reference: None,
700            extensions: _,
701        }) if schema_none_or_false(&validation.additional_properties)
702            && validation.max_properties.is_none()
703            && validation.min_properties.is_none()
704            && validation.pattern_properties.is_empty()
705            && validation.property_names.is_none() =>
706        {
707            Some((metadata, validation.as_ref()))
708        }
709
710        // Trivial (n == 1) subschemas
711        Schema::Object(SchemaObject {
712            metadata,
713            instance_type: _,
714            format: None,
715            enum_values: None,
716            const_value: None,
717            subschemas: Some(subschemas),
718            number: None,
719            string: None,
720            array: None,
721            object: None,
722            reference: None,
723            extensions: _,
724        }) => singleton_subschema(subschemas).and_then(|sub_schema| {
725            get_object(sub_schema).map(|(m, validation)| match m {
726                Some(_) => (metadata, validation),
727                None => (&None, validation),
728            })
729        }),
730
731        // None if the schema doesn't match the shape we expect.
732        _ => None,
733    }
734}
735
736// We infer from a Some(Schema::Bool(false)) or None value that either nothing
737// or nothing of importance is in the additional properties.
738fn schema_none_or_false(additional_properties: &Option<Box<Schema>>) -> bool {
739    matches!(
740        additional_properties.as_ref().map(Box::as_ref),
741        None | Some(Schema::Bool(false))
742    )
743}
744
745pub(crate) fn singleton_subschema(subschemas: &SubschemaValidation) -> Option<&Schema> {
746    match subschemas {
747        SubschemaValidation {
748            all_of: Some(subschemas),
749            any_of: None,
750            one_of: None,
751            not: None,
752            if_schema: None,
753            then_schema: None,
754            else_schema: None,
755        }
756        | SubschemaValidation {
757            all_of: None,
758            any_of: Some(subschemas),
759            one_of: None,
760            not: None,
761            if_schema: None,
762            then_schema: None,
763            else_schema: None,
764        }
765        | SubschemaValidation {
766            all_of: None,
767            any_of: None,
768            one_of: Some(subschemas),
769            not: None,
770            if_schema: None,
771            then_schema: None,
772            else_schema: None,
773        } if subschemas.len() == 1 => subschemas.first(),
774        _ => None,
775    }
776}
777
778pub(crate) enum Case {
779    Pascal,
780    Snake,
781}
782
783pub(crate) fn sanitize(input: &str, case: Case) -> String {
784    use heck::{ToPascalCase, ToSnakeCase};
785    let to_case = match case {
786        Case::Pascal => str::to_pascal_case,
787        Case::Snake => str::to_snake_case,
788    };
789
790    // If every case was special then none of them would be.
791    let out = match input {
792        "+1" => "plus1".to_string(),
793        "-1" => "minus1".to_string(),
794        _ => to_case(&input.replace("'", "").replace(|c| !is_xid_continue(c), "-")),
795    };
796
797    let prefix = to_case("x");
798
799    let out = match out.chars().next() {
800        None => prefix,
801        Some(c) if is_xid_start(c) => out,
802        Some(_) => format!("{}{}", prefix, out),
803    };
804
805    // Make sure the string is a valid Rust identifier.
806    if accept_as_ident(&out) {
807        out
808    } else {
809        format!("{}_", out)
810    }
811}
812
813/// Return true if the string is a valid Rust identifier.
814///
815/// If this function returns false, typify adds a trailing underscore to it. For
816/// example, `fn` becomes `fn_`.
817pub fn accept_as_ident(ident: &str) -> bool {
818    // Adapted from https://docs.rs/syn/2.0.114/src/syn/ident.rs.html#60-74. The
819    // main change is adding `gen` to the list.
820    match ident {
821        "_" |
822        // Based on https://doc.rust-lang.org/1.65.0/reference/keywords.html
823        "abstract" | "as" | "async" | "await" | "become" | "box" | "break" |
824        "const" | "continue" | "crate" | "do" | "dyn" | "else" | "enum" |
825        "extern" | "false" | "final" | "fn" | "for" | "gen" | "if" | "impl" |
826        "in" | "let" | "loop" | "macro" | "match" | "mod" | "move" | "mut" |
827        "override" | "priv" | "pub" | "ref" | "return" | "Self" | "self" |
828        "static" | "struct" | "super" | "trait" | "true" | "try" | "type" |
829        "typeof" | "unsafe" | "unsized" | "use" | "virtual" | "where" |
830        "while" | "yield" => false,
831        _ => true,
832    }
833}
834
835pub(crate) fn recase(input: &str, case: Case) -> (String, Option<String>) {
836    let new = sanitize(input, case);
837    let rename = if new == input {
838        None
839    } else {
840        Some(input.to_string())
841    };
842    (new, rename)
843}
844
845pub(crate) fn unique<I, T>(items: I) -> bool
846where
847    I: IntoIterator<Item = T>,
848    T: Eq + std::hash::Hash,
849{
850    let mut unique = HashSet::new();
851    items.into_iter().all(|item| unique.insert(item))
852}
853
854pub(crate) fn get_type_name(type_name: &Name, metadata: &Option<Box<Metadata>>) -> Option<String> {
855    let name = match (type_name, metadata_title(metadata)) {
856        (Name::Required(name), _) => name.clone(),
857        (Name::Suggested(name), None) => name.clone(),
858        (_, Some(name)) => name,
859        (Name::Unknown, None) => None?,
860    };
861
862    Some(sanitize(&name, Case::Pascal))
863}
864
865pub(crate) struct TypePatch {
866    pub name: String,
867    pub derives: BTreeSet<String>,
868    pub attrs: BTreeSet<String>,
869}
870
871impl TypePatch {
872    /// Creates a new TypePatch by resolving patches for the given type name.
873    pub fn new(type_space: &TypeSpace, type_name: String) -> Self {
874        match type_space.settings.patch.get(&type_name) {
875            None => Self {
876                name: type_name,
877                derives: Default::default(),
878                attrs: Default::default(),
879            },
880
881            Some(patch) => {
882                let name = patch.rename.clone().unwrap_or(type_name);
883                let derives = patch.derives.iter().cloned().collect();
884                let attrs = patch.attrs.iter().cloned().collect();
885
886                Self {
887                    name,
888                    derives,
889                    attrs,
890                }
891            }
892        }
893    }
894}
895
896pub(crate) struct StringValidator {
897    max_length: Option<u32>,
898    min_length: Option<u32>,
899    pattern: Option<regress::Regex>,
900}
901
902impl StringValidator {
903    pub fn new(type_name: &Name, validation: Option<&StringValidation>) -> Result<Self> {
904        let (max_length, min_length, pattern) =
905            validation.map_or(Ok((None, None, None)), |validation| {
906                let max = validation.max_length;
907                let min = validation.min_length;
908                let pattern = validation
909                    .pattern
910                    .as_ref()
911                    .map(|pattern| {
912                        regress::Regex::new(pattern).map_err(|e| Error::InvalidSchema {
913                            type_name: type_name.clone().into_option(),
914                            reason: format!("invalid pattern '{}' {}", pattern, e),
915                        })
916                    })
917                    .transpose()?;
918                Ok((max, min, pattern))
919            })?;
920        Ok(Self {
921            max_length,
922            min_length,
923            pattern,
924        })
925    }
926
927    pub fn is_valid<S: AsRef<str>>(&self, s: S) -> bool {
928        // Per the JSON Schema spec (and RFC 8259), minLength/maxLength count
929        // Unicode code points, not UTF-8 bytes, so we must not use `len()`.
930        self.max_length
931            .as_ref()
932            .is_none_or(|max| s.as_ref().chars().count() as u32 <= *max)
933            && self
934                .min_length
935                .as_ref()
936                .is_none_or(|min| s.as_ref().chars().count() as u32 >= *min)
937            && self
938                .pattern
939                .as_ref()
940                .is_none_or(|pattern| pattern.find(s.as_ref()).is_some())
941    }
942}
943
944/// A re-ordering of JSON schema instance types that puts integer values before
945/// number values.
946///
947/// This is used for untagged enum generation to ensure that integer values
948/// are matched before number values.
949#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
950pub(crate) enum ReorderedInstanceType {
951    /// The JSON schema instance type `null`.
952    Null,
953
954    /// The JSON schema instance type `boolean`.
955    Boolean,
956
957    /// The JSON schema instance type `integer`.
958    Integer,
959
960    /// The JSON schema instance type `number`.
961    Number,
962
963    /// The JSON schema instance type `string`.
964    String,
965
966    /// The JSON schema instance type `array`.
967    Array,
968
969    /// The JSON schema instance type `object`.
970    Object,
971}
972
973impl From<InstanceType> for ReorderedInstanceType {
974    fn from(instance_type: InstanceType) -> Self {
975        match instance_type {
976            InstanceType::Null => Self::Null,
977            InstanceType::Boolean => Self::Boolean,
978            InstanceType::Object => Self::Object,
979            InstanceType::Array => Self::Array,
980            InstanceType::Integer => Self::Integer,
981            InstanceType::Number => Self::Number,
982            InstanceType::String => Self::String,
983        }
984    }
985}
986
987impl From<ReorderedInstanceType> for InstanceType {
988    fn from(instance_type: ReorderedInstanceType) -> Self {
989        match instance_type {
990            ReorderedInstanceType::Null => Self::Null,
991            ReorderedInstanceType::Boolean => Self::Boolean,
992            ReorderedInstanceType::Object => Self::Object,
993            ReorderedInstanceType::Array => Self::Array,
994            ReorderedInstanceType::Integer => Self::Integer,
995            ReorderedInstanceType::Number => Self::Number,
996            ReorderedInstanceType::String => Self::String,
997        }
998    }
999}
1000
1001#[cfg(test)]
1002mod tests {
1003    use std::collections::BTreeMap;
1004
1005    use schemars::{
1006        gen::{SchemaGenerator, SchemaSettings},
1007        schema::StringValidation,
1008        schema_for, JsonSchema,
1009    };
1010
1011    use crate::{
1012        util::{
1013            all_mutually_exclusive, decode_segment, sanitize, schemas_mutually_exclusive, Case,
1014            ReorderedInstanceType,
1015        },
1016        Name,
1017    };
1018
1019    use super::StringValidator;
1020
1021    #[test]
1022    fn test_non_exclusive_structs() {
1023        #![allow(dead_code)]
1024
1025        #[derive(JsonSchema)]
1026        struct A {
1027            a: Option<()>,
1028            b: (),
1029        }
1030
1031        #[derive(JsonSchema)]
1032        struct B {
1033            a: (),
1034            b: Option<()>,
1035        }
1036
1037        let a = schema_for!(A).schema.into();
1038        let b = schema_for!(B).schema.into();
1039
1040        assert!(!schemas_mutually_exclusive(&a, &b, &BTreeMap::new()));
1041        assert!(!schemas_mutually_exclusive(&b, &a, &BTreeMap::new()));
1042    }
1043
1044    #[test]
1045    fn test_non_exclusive_oneof_subschema() {
1046        #![allow(dead_code)]
1047
1048        #[derive(JsonSchema)]
1049        enum A {
1050            B(i32),
1051            C(i64),
1052        }
1053
1054        let mut settings = SchemaSettings::default();
1055        settings.inline_subschemas = true;
1056        let gen = SchemaGenerator::new(settings);
1057
1058        let a = gen.into_root_schema_for::<Vec<A>>().schema.into();
1059
1060        assert!(!schemas_mutually_exclusive(&a, &a, &BTreeMap::new()));
1061    }
1062
1063    #[test]
1064    fn test_unique_prop_structs() {
1065        #![allow(dead_code)]
1066
1067        #[derive(JsonSchema)]
1068        struct A {
1069            a: Option<()>,
1070            b: (),
1071        }
1072
1073        #[derive(JsonSchema)]
1074        struct B {
1075            a: (),
1076            b: Option<()>,
1077            c: (),
1078        }
1079
1080        let a = schema_for!(A).schema.into();
1081        let b = schema_for!(B).schema.into();
1082
1083        assert!(schemas_mutually_exclusive(&a, &b, &BTreeMap::new()));
1084        assert!(schemas_mutually_exclusive(&b, &a, &BTreeMap::new()));
1085    }
1086
1087    #[test]
1088    fn test_exclusive_structs() {
1089        #![allow(dead_code)]
1090
1091        #[derive(JsonSchema)]
1092        struct A {
1093            a: Option<()>,
1094            b: (),
1095            aa: (),
1096        }
1097
1098        #[derive(JsonSchema)]
1099        struct B {
1100            a: (),
1101            b: Option<()>,
1102            bb: (),
1103        }
1104
1105        let a = schema_for!(A).schema.into();
1106        let b = schema_for!(B).schema.into();
1107
1108        assert!(schemas_mutually_exclusive(&a, &b, &BTreeMap::new()));
1109        assert!(schemas_mutually_exclusive(&b, &a, &BTreeMap::new()));
1110    }
1111
1112    #[test]
1113    fn test_exclusive_simple_arrays() {
1114        let a = schema_for!(Vec<u32>).schema.into();
1115        let b = schema_for!(Vec<f32>).schema.into();
1116
1117        assert!(schemas_mutually_exclusive(&a, &b, &BTreeMap::new()));
1118        assert!(schemas_mutually_exclusive(&b, &a, &BTreeMap::new()));
1119    }
1120
1121    #[test]
1122    fn test_all_mutually_exclusive_empty() {
1123        assert!(all_mutually_exclusive(&[], &BTreeMap::new()));
1124    }
1125
1126    #[test]
1127    fn test_decode_segment() {
1128        assert_eq!(decode_segment("foo~1bar"), "foo/bar");
1129        assert_eq!(decode_segment("foo~0bar"), "foo~bar");
1130    }
1131
1132    #[test]
1133    fn test_sanitize() {
1134        assert_eq!(sanitize("type", Case::Snake), "type_");
1135        assert_eq!(sanitize("ref", Case::Snake), "ref_");
1136        assert_eq!(sanitize("gen", Case::Snake), "gen_");
1137        assert_eq!(sanitize("gen", Case::Pascal), "Gen");
1138        assert_eq!(sanitize("+1", Case::Snake), "plus1");
1139        assert_eq!(sanitize("-1", Case::Snake), "minus1");
1140        assert_eq!(sanitize("@timestamp", Case::Pascal), "Timestamp");
1141        assert_eq!(sanitize("won't and can't", Case::Pascal), "WontAndCant");
1142        assert_eq!(
1143            sanitize(
1144                "urn:ietf:params:scim:schemas:extension:gluu:2.0:user_",
1145                Case::Pascal
1146            ),
1147            "UrnIetfParamsScimSchemasExtensionGluu20User"
1148        );
1149        assert_eq!(sanitize("Ipv6Net", Case::Snake), "ipv6_net");
1150        assert_eq!(sanitize("V6", Case::Pascal), "V6");
1151    }
1152
1153    #[test]
1154    fn test_string_validation() {
1155        let permissive = StringValidator::new(&Name::Unknown, None).unwrap();
1156        assert!(permissive.is_valid("everything should be fine"));
1157        assert!(permissive.is_valid(""));
1158
1159        let also_permissive = StringValidator::new(
1160            &Name::Unknown,
1161            Some(&StringValidation {
1162                max_length: None,
1163                min_length: None,
1164                pattern: None,
1165            }),
1166        )
1167        .unwrap();
1168        assert!(also_permissive.is_valid("everything should be fine"));
1169        assert!(also_permissive.is_valid(""));
1170
1171        let eight = StringValidator::new(
1172            &Name::Unknown,
1173            Some(&StringValidation {
1174                max_length: Some(8),
1175                min_length: Some(8),
1176                pattern: None,
1177            }),
1178        )
1179        .unwrap();
1180        assert!(eight.is_valid("Shadrach"));
1181        assert!(!eight.is_valid("Meshach"));
1182        assert!(eight.is_valid("Abednego"));
1183
1184        let ach = StringValidator::new(
1185            &Name::Unknown,
1186            Some(&StringValidation {
1187                max_length: None,
1188                min_length: None,
1189                pattern: Some("ach$".to_string()),
1190            }),
1191        )
1192        .unwrap();
1193        assert!(ach.is_valid("Shadrach"));
1194        assert!(ach.is_valid("Meshach"));
1195        assert!(!ach.is_valid("Abednego"));
1196    }
1197
1198    #[test]
1199    fn test_string_validation_multi_byte() {
1200        // minLength/maxLength count Unicode code points, not UTF-8 bytes.
1201        // "héllo" is 5 code points but 6 bytes (the "é" is 2 bytes).
1202        let five = StringValidator::new(
1203            &Name::Unknown,
1204            Some(&StringValidation {
1205                max_length: Some(5),
1206                min_length: Some(5),
1207                pattern: None,
1208            }),
1209        )
1210        .unwrap();
1211        assert!(five.is_valid("héllo"));
1212        assert!(five.is_valid("hello"));
1213        // 6 bytes but only 5 code points -- should still be valid.
1214        assert_eq!("héllo".len(), 6);
1215        assert_eq!("héllo".chars().count(), 5);
1216
1217        // A single 4-byte emoji is one code point, so it satisfies a
1218        // minLength/maxLength of 1 even though it is 4 bytes long.
1219        let one = StringValidator::new(
1220            &Name::Unknown,
1221            Some(&StringValidation {
1222                max_length: Some(1),
1223                min_length: Some(1),
1224                pattern: None,
1225            }),
1226        )
1227        .unwrap();
1228        assert!(one.is_valid("🍔"));
1229        assert_eq!("🍔".len(), 4);
1230        assert_eq!("🍔".chars().count(), 1);
1231        // Two code points should now be rejected by maxLength: 1.
1232        assert!(!one.is_valid("🍔🍔"));
1233
1234        // A string of exactly 8 code points, each multi-byte, should pass
1235        // an exact-length-8 validator even though its byte length is 24.
1236        let eight = StringValidator::new(
1237            &Name::Unknown,
1238            Some(&StringValidation {
1239                max_length: Some(8),
1240                min_length: Some(8),
1241                pattern: None,
1242            }),
1243        )
1244        .unwrap();
1245        let emoji8 = "🍔".repeat(8);
1246        assert_eq!(emoji8.len(), 32);
1247        assert_eq!(emoji8.chars().count(), 8);
1248        assert!(eight.is_valid(&emoji8));
1249        assert!(!eight.is_valid("🍔".repeat(7)));
1250        assert!(!eight.is_valid("🍔".repeat(9)));
1251    }
1252
1253    #[test]
1254    fn test_instance_type_ordering() {
1255        let null = ReorderedInstanceType::Null;
1256        let boolean = ReorderedInstanceType::Boolean;
1257        let integer = ReorderedInstanceType::Integer;
1258        let number = ReorderedInstanceType::Number;
1259        let string = ReorderedInstanceType::String;
1260        let array = ReorderedInstanceType::Array;
1261        let object = ReorderedInstanceType::Object;
1262
1263        assert!(null < boolean);
1264        assert!(boolean < integer);
1265        assert!(integer < number);
1266        assert!(number < string);
1267        assert!(string < array);
1268        assert!(array < object);
1269    }
1270}