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}