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}