vector_config/schema/
helpers.rs

1use std::{
2    cell::RefCell,
3    collections::{BTreeSet, HashMap},
4    env, mem,
5};
6
7use indexmap::IndexMap;
8use serde_json::{Map, Value};
9use vector_config_common::{attributes::CustomAttribute, constants, schema::*};
10
11use super::visitors::{
12    DisallowUnevaluatedPropertiesVisitor, GenerateHumanFriendlyNameVisitor,
13    InlineSingleUseReferencesVisitor,
14};
15use crate::{
16    Configurable, ConfigurableRef, GenerateError, Metadata, ToValue, num::ConfigurableNumber,
17};
18
19/// Applies metadata to the given schema.
20///
21/// Metadata can include semantic information (title, description, etc), validation (min/max, allowable
22/// patterns, etc), as well as actual arbitrary key/value data.
23pub fn apply_base_metadata(schema: &mut SchemaObject, metadata: Metadata) {
24    apply_metadata(&<()>::as_configurable_ref(), schema, metadata)
25}
26
27fn apply_metadata(config: &ConfigurableRef, schema: &mut SchemaObject, metadata: Metadata) {
28    let type_name = config.type_name();
29    let base_metadata = config.make_metadata();
30
31    // Calculate the title/description of this schema.
32    //
33    // If the given `metadata` has either a title or description present, we use both those values,
34    // even if one of them is `None`. If both are `None`, we try falling back to the base metadata
35    // for the configurable type.
36    //
37    // This ensures that per-field titles/descriptions can override the base title/description of
38    // the type, without mixing and matching, as sometimes the base type's title/description is far
39    // too generic and muddles the output. Essentially, if the callsite decides to provide an
40    // overridden title/description, it controls the entire title/description.
41    let (schema_title, schema_description) =
42        if metadata.title().is_some() || metadata.description().is_some() {
43            (metadata.title(), metadata.description())
44        } else {
45            (base_metadata.title(), base_metadata.description())
46        };
47
48    // A description _must_ be present, one way or another, _unless_ one of these two conditions is
49    // met:
50    // - the field is marked transparent
51    // - the type is referenceable and _does_ have a description
52    //
53    // We panic otherwise.
54    let has_referenceable_description =
55        config.referenceable_name().is_some() && base_metadata.description().is_some();
56    let is_transparent = base_metadata.transparent() || metadata.transparent();
57    if schema_description.is_none() && !is_transparent && !has_referenceable_description {
58        panic!(
59            "No description provided for `{type_name}`! All `Configurable` types must define a description, or have one specified at the field-level where the type is being used."
60        );
61    }
62
63    // If a default value was given, serialize it.
64    let schema_default = metadata.default_value().map(ToValue::to_value);
65
66    // Take the existing schema metadata, if any, or create a default version of it, and then apply
67    // all of our newly-calculated values to it.
68    //
69    // Similar to the above title/description logic, we update both title/description if either of
70    // them have been set, to avoid mixing/matching between base and override metadata.
71    let mut schema_metadata = schema.metadata.take().unwrap_or_default();
72    if schema_title.is_some() || schema_description.is_some() {
73        schema_metadata.title = schema_title.map(|s| s.to_string());
74        schema_metadata.description = schema_description.map(|s| s.to_string());
75    }
76    schema_metadata.default = schema_default.or(schema_metadata.default);
77    schema_metadata.deprecated = metadata.deprecated();
78
79    // Set any custom attributes as extensions on the schema. If an attribute is declared multiple
80    // times, we turn the value into an array and merge them together. We _do_ not that, however, if
81    // the original value is a flag, or the value being added to an existing key is a flag, as
82    // having a flag declared multiple times, or mixing a flag with a KV pair, doesn't make sense.
83    let map_entries_len = {
84        let custom_map = schema
85            .extensions
86            .entry("_metadata".to_string())
87            .or_insert_with(|| Value::Object(Map::new()))
88            .as_object_mut()
89            .expect("metadata extension must always be a map");
90
91        if let Some(message) = metadata.deprecated_message() {
92            custom_map.insert(
93                "deprecated_message".to_string(),
94                serde_json::Value::String(message.to_string()),
95            );
96        }
97
98        for attribute in metadata.custom_attributes() {
99            match attribute {
100                CustomAttribute::Flag(key) => {
101                    match custom_map.insert(key.to_string(), Value::Bool(true)) {
102                        // Overriding a flag is fine, because flags are only ever "enabled", so there's
103                        // no harm to enabling it... again. Likewise, if there was no existing value,
104                        // it's fine.
105                        Some(Value::Bool(_)) | None => {}
106                        // Any other value being present means we're clashing with a different metadata
107                        // attribute, which is not good, so we have to bail out.
108                        _ => panic!(
109                            "Tried to set metadata flag '{key}' but already existed in schema metadata for `{type_name}`."
110                        ),
111                    }
112                }
113                CustomAttribute::KeyValue { key, value } => {
114                    custom_map.entry(key.to_string())
115                        .and_modify(|existing_value| match existing_value {
116                            // We already have a flag entry for this key, which we cannot turn into an
117                            // array, so we panic in this particular case to signify the weirdness.
118                            Value::Bool(_) => {
119                                panic!("Tried to overwrite metadata flag '{key}' but already existed in schema metadata for `{type_name}` as a flag.");
120                            },
121                            // The entry is already a multi-value KV pair, so just append the value.
122                            Value::Array(items) => {
123                                items.push(value.clone());
124                            },
125                            // The entry is not already a multi-value KV pair, so turn it into one.
126                            _ => {
127                                let taken_existing_value = std::mem::replace(existing_value, Value::Null);
128                                *existing_value = Value::Array(vec![taken_existing_value, value.clone()]);
129                            },
130                        })
131                        .or_insert(value.clone());
132                }
133            }
134        }
135
136        custom_map.len()
137    };
138
139    // If the schema had no existing metadata, and we didn't add any of our own, then remove the
140    // metadata extension property entirely, as it would only add noise to the schema output.
141    if map_entries_len == 0 {
142        schema.extensions.remove("_metadata");
143    }
144
145    // Now apply any relevant validations.
146    for validation in metadata.validations() {
147        validation.apply(schema);
148    }
149
150    schema.metadata = Some(schema_metadata);
151}
152
153pub fn convert_to_flattened_schema(primary: &mut SchemaObject, mut subschemas: Vec<SchemaObject>) {
154    // First, we replace the primary schema with an empty schema, because we need to push it the actual primary schema
155    // into the list of `allOf` schemas. This is due to the fact that it's not valid to "extend" a schema using `allOf`,
156    // so everything has to be in there.
157    let primary_subschema = mem::take(primary);
158    subschemas.insert(0, primary_subschema);
159
160    let all_of_schemas = subschemas.into_iter().map(Schema::Object).collect();
161
162    // Now update the primary schema to use `allOf` to bring everything together.
163    primary.subschemas = Some(Box::new(SubschemaValidation {
164        all_of: Some(all_of_schemas),
165        ..Default::default()
166    }));
167}
168
169pub fn generate_null_schema() -> SchemaObject {
170    SchemaObject {
171        instance_type: Some(InstanceType::Null.into()),
172        ..Default::default()
173    }
174}
175
176pub fn generate_bool_schema() -> SchemaObject {
177    SchemaObject {
178        instance_type: Some(InstanceType::Boolean.into()),
179        ..Default::default()
180    }
181}
182
183pub fn generate_string_schema() -> SchemaObject {
184    SchemaObject {
185        instance_type: Some(InstanceType::String.into()),
186        ..Default::default()
187    }
188}
189
190pub fn generate_number_schema<N>() -> SchemaObject
191where
192    N: ConfigurableNumber,
193{
194    // TODO: Once `schemars` has proper integer support, we should allow specifying min/max bounds
195    // in a way that's relevant to the number class. As is, we're always forcing bounds to fit into
196    // `f64` regardless of whether or not we're using `u64` vs `f64` vs `i16`, and so on.
197    let minimum = N::get_enforced_min_bound();
198    let maximum = N::get_enforced_max_bound();
199
200    // We always set the minimum/maximum bound to the mechanical limits. Any additional constraining as part of field
201    // validators will overwrite these limits.
202    let mut schema = SchemaObject {
203        instance_type: Some(N::class().as_instance_type().into()),
204        number: Some(Box::new(NumberValidation {
205            minimum: Some(minimum),
206            maximum: Some(maximum),
207            ..Default::default()
208        })),
209        ..Default::default()
210    };
211
212    // If the actual numeric type we're generating the schema for is a nonzero variant, and its constraint can't be
213    // represented solely by the normal minimum/maximum bounds, we explicitly add an exclusion for the appropriate zero
214    // value of the given numeric type.
215    if N::requires_nonzero_exclusion() {
216        schema.subschemas = Some(Box::new(SubschemaValidation {
217            not: Some(Box::new(Schema::Object(SchemaObject {
218                const_value: Some(Value::Number(N::get_encoded_zero_value())),
219                ..Default::default()
220            }))),
221            ..Default::default()
222        }));
223    }
224
225    schema
226}
227
228pub(crate) fn generate_array_schema(
229    config: &ConfigurableRef,
230    generator: &RefCell<SchemaGenerator>,
231) -> Result<SchemaObject, GenerateError> {
232    // Generate the actual schema for the element type.
233    let element_schema = get_or_generate_schema(config, generator, None)?;
234
235    Ok(SchemaObject {
236        instance_type: Some(InstanceType::Array.into()),
237        array: Some(Box::new(ArrayValidation {
238            items: Some(SingleOrVec::Single(Box::new(element_schema.into()))),
239            ..Default::default()
240        })),
241        ..Default::default()
242    })
243}
244
245pub(crate) fn generate_set_schema(
246    config: &ConfigurableRef,
247    generator: &RefCell<SchemaGenerator>,
248) -> Result<SchemaObject, GenerateError> {
249    // Generate the actual schema for the element type.
250    let element_schema = get_or_generate_schema(config, generator, None)?;
251
252    Ok(SchemaObject {
253        instance_type: Some(InstanceType::Array.into()),
254        array: Some(Box::new(ArrayValidation {
255            items: Some(SingleOrVec::Single(Box::new(element_schema.into()))),
256            unique_items: Some(true),
257            ..Default::default()
258        })),
259        ..Default::default()
260    })
261}
262
263pub(crate) fn generate_map_schema(
264    config: &ConfigurableRef,
265    generator: &RefCell<SchemaGenerator>,
266) -> Result<SchemaObject, GenerateError> {
267    // Generate the actual schema for the element type.
268    let element_schema = get_or_generate_schema(config, generator, None)?;
269
270    Ok(SchemaObject {
271        instance_type: Some(InstanceType::Object.into()),
272        object: Some(Box::new(ObjectValidation {
273            additional_properties: Some(Box::new(element_schema.into())),
274            ..Default::default()
275        })),
276        ..Default::default()
277    })
278}
279
280pub fn generate_struct_schema(
281    properties: IndexMap<String, SchemaObject>,
282    required: BTreeSet<String>,
283    additional_properties: Option<Box<Schema>>,
284) -> SchemaObject {
285    let properties = properties
286        .into_iter()
287        .map(|(k, v)| (k, Schema::Object(v)))
288        .collect();
289    SchemaObject {
290        instance_type: Some(InstanceType::Object.into()),
291        object: Some(Box::new(ObjectValidation {
292            properties,
293            required,
294            additional_properties,
295            ..Default::default()
296        })),
297        ..Default::default()
298    }
299}
300
301pub(crate) fn generate_optional_schema(
302    config: &ConfigurableRef,
303    generator: &RefCell<SchemaGenerator>,
304) -> Result<SchemaObject, GenerateError> {
305    // Optional schemas are generally very simple in practice, but because of how we memoize schema
306    // generation and use references to schema definitions, we have to handle quite a few cases
307    // here.
308    //
309    // Specifically, for the `T` in `Option<T>`, we might be dealing with:
310    // - a scalar type, where we're going to emit a schema that has `"type": ["string","null"]`, or
311    //   something to that effect, where we can simply add the `"`null"` instance type and be done
312    // - we may have a referenceable type (i.e. `struct FooBar`) and then we need to generate the
313    //   schema for that referenceable type and either:
314    //   - append a "null" schema as a `oneOf`/`anyOf` if the generated schema for the referenceable
315    //     type already uses that mechanism
316    //   - create our own `oneOf` schema to map between either the "null" schema or the real schema
317
318    // Generate the inner schema for the inner type. We'll add some override metadata, too, so that
319    // we can mark this resulting schema as "optional". This is only consequential to documentation
320    // generation so that some of the more complex code for parsing enum schemas can correctly
321    // differentiate a `oneOf` schema that represents a Rust enum versus one that simply represents
322    // our "null or X" wrapped schema.
323    let mut overrides = Metadata::default();
324    overrides.add_custom_attribute(CustomAttribute::flag(constants::DOCS_META_OPTIONAL));
325    let mut schema = get_or_generate_schema(config, generator, Some(overrides))?;
326
327    // Take the metadata and extensions of the original schema.
328    //
329    // We'll apply these back to `schema` at the end, which will either place them back where they
330    // came from (if we don't have to wrap the original schema) or will apply them to the new
331    // wrapped schema.
332    let original_metadata = schema.metadata.take();
333    let original_extensions = std::mem::take(&mut schema.extensions);
334
335    // Figure out if the schema is a referenceable schema or a scalar schema.
336    match schema.instance_type.as_mut() {
337        // If the schema has no instance types, this implies it's a non-scalar schema: it references
338        // another schema, or it's a composite schema/does subschema validation (`$ref`, `oneOf`,
339        // `anyOf`, etc).
340        //
341        // Figure out which it is, and either modify the schema or generate a new schema accordingly.
342        None => match schema.subschemas.as_mut() {
343            None => {
344                // If we don't have a scalar schema, or a schema that uses subschema validation,
345                // then we simply create a new schema that uses `oneOf` to allow mapping to either
346                // the existing schema _or_ a null schema.
347                //
348                // This should handle all cases of "normal" referenceable schema types.
349                let wrapped_schema = SchemaObject {
350                    subschemas: Some(Box::new(SubschemaValidation {
351                        one_of: Some(vec![
352                            Schema::Object(generate_null_schema()),
353                            Schema::Object(std::mem::take(&mut schema)),
354                        ]),
355                        ..Default::default()
356                    })),
357                    ..Default::default()
358                };
359
360                schema = wrapped_schema;
361            }
362            Some(subschemas) => {
363                if let Some(any_of) = subschemas.any_of.as_mut() {
364                    // A null schema is just another possible variant, so we add it directly.
365                    any_of.push(Schema::Object(generate_null_schema()));
366                } else if let Some(one_of) = subschemas.one_of.as_mut() {
367                    // A null schema is just another possible variant, so we add it directly.
368                    one_of.push(Schema::Object(generate_null_schema()));
369                } else if subschemas.all_of.is_some() {
370                    // If we're dealing with an all-of schema, we have to build a new one-of schema
371                    // where the two choices are either the `null` schema, or a subschema comprised of
372                    // the all-of subschemas.
373                    let all_of = subschemas
374                        .all_of
375                        .take()
376                        .expect("all-of subschemas must be present here");
377                    let new_all_of_schema = SchemaObject {
378                        subschemas: Some(Box::new(SubschemaValidation {
379                            all_of: Some(all_of),
380                            ..Default::default()
381                        })),
382                        ..Default::default()
383                    };
384
385                    subschemas.one_of = Some(vec![
386                        Schema::Object(generate_null_schema()),
387                        Schema::Object(new_all_of_schema),
388                    ]);
389                } else {
390                    return Err(GenerateError::InvalidOptionalSchema);
391                }
392            }
393        },
394        Some(sov) => match sov {
395            SingleOrVec::Single(ty) if **ty != InstanceType::Null => {
396                *sov = vec![**ty, InstanceType::Null].into()
397            }
398            SingleOrVec::Vec(ty) if !ty.contains(&InstanceType::Null) => {
399                ty.push(InstanceType::Null)
400            }
401            _ => {}
402        },
403    }
404
405    // Stick the metadata and extensions back on `schema`.
406    schema.metadata = original_metadata;
407    schema.extensions = original_extensions;
408
409    Ok(schema)
410}
411
412pub fn generate_one_of_schema(subschemas: &[SchemaObject]) -> SchemaObject {
413    let subschemas = subschemas
414        .iter()
415        .map(|s| Schema::Object(s.clone()))
416        .collect::<Vec<_>>();
417
418    SchemaObject {
419        subschemas: Some(Box::new(SubschemaValidation {
420            one_of: Some(subschemas),
421            ..Default::default()
422        })),
423        ..Default::default()
424    }
425}
426
427pub fn generate_any_of_schema(subschemas: &[SchemaObject]) -> SchemaObject {
428    let subschemas = subschemas
429        .iter()
430        .map(|s| Schema::Object(s.clone()))
431        .collect::<Vec<_>>();
432
433    SchemaObject {
434        subschemas: Some(Box::new(SubschemaValidation {
435            any_of: Some(subschemas),
436            ..Default::default()
437        })),
438        ..Default::default()
439    }
440}
441
442pub fn generate_tuple_schema(subschemas: &[SchemaObject]) -> SchemaObject {
443    let subschemas = subschemas
444        .iter()
445        .map(|s| Schema::Object(s.clone()))
446        .collect::<Vec<_>>();
447
448    SchemaObject {
449        instance_type: Some(InstanceType::Array.into()),
450        array: Some(Box::new(ArrayValidation {
451            items: Some(SingleOrVec::Vec(subschemas)),
452            // Rust's tuples are closed -- fixed size -- so we set `additionalItems` such that any
453            // items past what we have in `items` will cause schema validation to fail.
454            additional_items: Some(Box::new(Schema::Bool(false))),
455            ..Default::default()
456        })),
457        ..Default::default()
458    }
459}
460
461pub fn generate_enum_schema(values: Vec<Value>) -> SchemaObject {
462    SchemaObject {
463        enum_values: Some(values),
464        ..Default::default()
465    }
466}
467
468pub fn generate_const_string_schema(value: String) -> SchemaObject {
469    SchemaObject {
470        const_value: Some(Value::String(value)),
471        ..Default::default()
472    }
473}
474
475pub fn generate_internal_tagged_variant_schema(
476    tag: String,
477    value_schema: SchemaObject,
478) -> SchemaObject {
479    let mut properties = IndexMap::new();
480    properties.insert(tag.clone(), value_schema);
481
482    let mut required = BTreeSet::new();
483    required.insert(tag);
484
485    generate_struct_schema(properties, required, None)
486}
487
488pub fn default_schema_settings() -> SchemaSettings {
489    SchemaSettings::new()
490        .with_visitor(InlineSingleUseReferencesVisitor::from_settings)
491        .with_visitor(DisallowUnevaluatedPropertiesVisitor::from_settings)
492        .with_visitor(GenerateHumanFriendlyNameVisitor::from_settings)
493}
494
495pub fn generate_root_schema<T>() -> Result<RootSchema, GenerateError>
496where
497    T: Configurable + 'static,
498{
499    generate_root_schema_with_settings::<T>(default_schema_settings())
500}
501
502pub fn generate_root_schema_with_settings<T>(
503    schema_settings: SchemaSettings,
504) -> Result<RootSchema, GenerateError>
505where
506    T: Configurable + 'static,
507{
508    let schema_gen = RefCell::new(schema_settings.into_generator());
509
510    // Set env variable to enable generating all schemas, including platform-specific ones.
511    unsafe { env::set_var("VECTOR_GENERATE_SCHEMA", "true") };
512
513    let schema =
514        get_or_generate_schema(&T::as_configurable_ref(), &schema_gen, Some(T::metadata()))?;
515
516    unsafe { env::remove_var("VECTOR_GENERATE_SCHEMA") };
517
518    Ok(schema_gen.into_inner().into_root_schema(schema))
519}
520
521pub fn get_or_generate_schema(
522    config: &ConfigurableRef,
523    generator: &RefCell<SchemaGenerator>,
524    overrides: Option<Metadata>,
525) -> Result<SchemaObject, GenerateError> {
526    let metadata = config.make_metadata();
527    let (mut schema, metadata) = match config.referenceable_name() {
528        // When the configurable type has a referenceable name, try looking it up in the schema
529        // generator's definition list, and if it exists, create a schema reference to
530        // it. Otherwise, generate it and backfill it in the schema generator.
531        Some(name) => {
532            if !generator.borrow().definitions().contains_key(name) {
533                // In order to avoid infinite recursion, we copy the approach that `schemars` takes and
534                // insert a dummy boolean schema before actually generating the real schema, and then
535                // replace it afterwards. If any recursion occurs, a schema reference will be handed
536                // back, which means we don't have to worry about the dummy schema needing to be updated
537                // after the fact.
538                generator
539                    .borrow_mut()
540                    .definitions_mut()
541                    .insert(name.to_string(), Schema::Bool(false));
542
543                // We generate the schema for the type with its own default metadata, and not the
544                // override metadata passed into this method, because the override metadata might
545                // only be relevant to the place that the type is being used.
546                //
547                // For example, if the type was something for setting the logging level, one
548                // component that allows the logging level to be changed for that component
549                // specifically might want to specify a default value, whereas the configurable
550                // should not have a default at all.  So, if we applied that override metadata, we'd
551                // be unwittingly applying a default for all usages of the type that didn't override
552                // the default themselves.
553                let mut schema = config.generate_schema(generator)?;
554                apply_metadata(config, &mut schema, metadata);
555
556                generator
557                    .borrow_mut()
558                    .definitions_mut()
559                    .insert(name.to_string(), Schema::Object(schema));
560            }
561
562            (get_schema_ref(generator, name), None)
563        }
564        // Always generate the schema directly if the type is not referenceable.
565        None => (config.generate_schema(generator)?, Some(metadata)),
566    };
567
568    // Figure out what metadata we should apply to the resulting schema.
569    //
570    // If the type was referenceable, we use its implicit metadata when generating the
571    // "baseline" schema, because a referenceable type should always be self-contained. We then
572    // apply the override metadata, if it exists, to the schema we got back. This allows us to
573    // override titles, descriptions, and add additional attributes, and so on.
574    //
575    // If the type was not referenceable, we only generate its schema without trying to apply any
576    // metadata. We do that because applying schema metadata enforces logic like "can't be without a
577    // description". The implicit metadata for the type may lack that.
578    if let Some(overrides) = overrides.as_ref() {
579        config.validate_metadata(overrides)?;
580    }
581
582    match metadata {
583        // If we generated the schema for a referenceable type, we won't need to merge its implicit
584        // metadata into the schema we're returning _here_, so just use the override metadata if
585        // it was given.
586        None => {
587            if let Some(metadata) = overrides {
588                apply_metadata(config, &mut schema, metadata);
589            }
590        }
591
592        // If we didn't generate the schema for a referenceable type, we'll be holding its implicit
593        // metadata here, which we need to merge the override metadata into if it was given. If
594        // there was no override metadata, then we just use the base by itself.
595        Some(base) => match overrides {
596            None => apply_metadata(config, &mut schema, base),
597            Some(overrides) => apply_metadata(config, &mut schema, base.merge(overrides)),
598        },
599    };
600
601    Ok(schema)
602}
603
604fn get_schema_ref<S: AsRef<str>>(generator: &RefCell<SchemaGenerator>, name: S) -> SchemaObject {
605    let ref_path = format!(
606        "{}{}",
607        generator.borrow().settings().definitions_path(),
608        name.as_ref()
609    );
610    SchemaObject::new_ref(ref_path)
611}
612
613/// Asserts that the key type `K` generates a string-like schema, suitable for use in maps.
614///
615/// This function generates a schema for `K` and ensures that the resulting schema is explicitly,
616/// but only, represented as a `string` data type. This is necessary to ensure that `K` can be used
617/// as the key type for maps, as maps are represented by the `object` data type in JSON Schema,
618/// which must have fields with valid string identifiers.
619///
620/// # Errors
621///
622/// If the schema is not a valid, string-like schema, an error variant will be returned describing
623/// the issue.
624pub(crate) fn assert_string_schema_for_map(
625    config: &ConfigurableRef,
626    generator: &RefCell<SchemaGenerator>,
627    map_type: &'static str,
628) -> Result<(), GenerateError> {
629    let key_schema = get_or_generate_schema(config, generator, None)?;
630    let key_type = config.type_name();
631
632    // We need to force the schema to be treated as transparent so that when the schema generation
633    // finalizes the schema, we don't throw an error due to a lack of title/description.
634    let mut key_metadata = Metadata::default();
635    key_metadata.set_transparent();
636
637    let wrapped_schema = Schema::Object(key_schema);
638
639    // Get a reference to the underlying schema if we're dealing with a reference, or just use what
640    // we have if it's the actual definition.
641    let generator = generator.borrow();
642    let underlying_schema = if wrapped_schema.is_ref() {
643        generator.dereference(&wrapped_schema)
644    } else {
645        Some(&wrapped_schema)
646    };
647
648    let is_string_like = match underlying_schema {
649        Some(Schema::Object(schema_object)) => match schema_object.instance_type.as_ref() {
650            Some(sov) => match sov {
651                // Has to be a string.
652                SingleOrVec::Single(it) => **it == InstanceType::String,
653                // As long as there's only one instance type, and it's string, we're fine
654                // with that, too.
655                SingleOrVec::Vec(its) => {
656                    its.len() == 1
657                        && its
658                            .first()
659                            .filter(|it| *it == &InstanceType::String)
660                            .is_some()
661                }
662            },
663            // We match explicitly, so a lack of declared instance types is not considered
664            // valid here.
665            None => false,
666        },
667        // We match explicitly, so boolean schemas aren't considered valid here.
668        _ => false,
669    };
670
671    if !is_string_like {
672        Err(GenerateError::MapKeyNotStringLike { key_type, map_type })
673    } else {
674        Ok(())
675    }
676}
677
678/// Determines whether an enum schema is ambiguous based on discriminants of its variants.
679///
680/// A discriminant is the set of the named fields which are required, which may be an empty set.
681pub fn has_ambiguous_discriminants(
682    discriminants: &HashMap<&'static str, BTreeSet<String>>,
683) -> bool {
684    // Firstly, if there's less than two discriminants, then there can't be any ambiguity.
685    if discriminants.len() < 2 {
686        return false;
687    }
688
689    // Any empty discriminant is considered ambiguous.
690    if discriminants
691        .values()
692        .any(|discriminant| discriminant.is_empty())
693    {
694        return true;
695    }
696
697    // Now collapse the list of discriminants into another set, which will eliminate any duplicate
698    // sets. If there are any duplicate sets, this would also imply ambiguity, since there's not
699    // enough discrimination via required fields.
700    let deduplicated = discriminants.values().cloned().collect::<BTreeSet<_>>();
701    if deduplicated.len() != discriminants.len() {
702        return true;
703    }
704
705    false
706}