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