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