Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 2 additions & 7 deletions lib/docs-renderer/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,8 @@ fn main() -> Result<()> {
// Find the base component schema for the component type itself, which is analogous to
// `SourceOuter`, `SinkOuter`, etc. We render the schema for that separately as it's meant
// to be common across components of the same type, etc.
let base_component_schema = querier
.query()
.with_custom_attribute_kv(
constants::DOCS_META_COMPONENT_BASE_TYPE,
base_component_type,
)
.run_single()?;
let base_component_schema =
querier.root_map_value_schema(&format!("{}s", base_component_type.as_str()))?;

debug!(
"Got base component schema for component type '{}'.",
Expand Down
1 change: 0 additions & 1 deletion lib/vector-config-common/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ pub const COMPONENT_TYPE_SOURCE: &str = "source";
pub const COMPONENT_TYPE_TRANSFORM: &str = "transform";
pub const COMPONENT_TYPE_GLOBAL_OPTION: &str = "global_option";
pub const DOCS_META_ADDITIONAL_PROPS_DESC: &str = "docs::additional_props_description";
pub const DOCS_META_COMPONENT_BASE_TYPE: &str = "docs::component_base_type";
pub const DOCS_META_COMPONENT_NAME: &str = "docs::component_name";
pub const DOCS_META_COMPONENT_TYPE: &str = "docs::component_type";
pub const DOCS_META_ENUM_CONTENT_FIELD: &str = "docs::enum_content_field";
Expand Down
1 change: 1 addition & 0 deletions lib/vector-config-common/src/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

mod generator;
mod json_schema;
mod navigation;
pub mod visit;

pub(crate) const DEFINITIONS_PREFIX: &str = "#/definitions/";
Expand Down
93 changes: 93 additions & 0 deletions lib/vector-config-common/src/schema/navigation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use super::{DEFINITIONS_PREFIX, RootSchema, Schema, SchemaObject, Set};

impl RootSchema {
/// Finds the value schema of a named map at the configuration root.
///
/// Follows local references and `allOf` (flattened fields), but does not
/// descend into nested properties or choose between union alternatives.
pub fn root_map_value_schema(&self, property: &str) -> Option<&Schema> {
for schema in self.object_schemas(&self.schema) {
let Some(map) = schema
.object
.as_ref()
.and_then(|object| object.properties.get(property))
.and_then(Schema::as_object)
else {
continue;
};
if let Some(value) = self
.object_schemas(map)
.find_map(|schema| schema.object.as_ref()?.additional_properties.as_deref())
{
return Some(value);
}
}
None
}

fn object_schemas<'a>(
&'a self,
schema: &'a SchemaObject,
) -> impl Iterator<Item = &'a SchemaObject> {
let mut pending = vec![schema];
let mut seen = Set::new();
std::iter::from_fn(move || {
let schema = pending.pop()?;
if let Some(all_of) = schema.subschemas.as_ref().and_then(|s| s.all_of.as_ref()) {
pending.extend(all_of.iter().rev().filter_map(Schema::as_object));
}
if let Some(name) = schema
.reference
.as_deref()
.and_then(|r| r.strip_prefix(DEFINITIONS_PREFIX))
&& seen.insert(name)
&& let Some(target) = self.definitions.get(name).and_then(Schema::as_object)
{
pending.push(target);
}
Some(schema)
})
}
}

#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;

#[test]
fn root_map_lookup_follows_flattening_and_references() {
let root: RootSchema = serde_json::from_value(json!({
"allOf": [{"$ref": "#/definitions/config"}],
"definitions": {
"config": {"properties": {"sources": {"$ref": "#/definitions/map"}}},
"map": {"type": "object", "additionalProperties": {"$ref": "#/definitions/outer"}},
"outer": {"type": "object"}
}
}))
.unwrap();
assert_eq!(
root.root_map_value_schema("sources")
.unwrap()
.as_object()
.unwrap()
.reference
.as_deref(),
Some("#/definitions/outer")
);
assert!(root.root_map_value_schema("sinks").is_none());
}

#[test]
fn root_map_lookup_does_not_search_nested_properties_or_loop_on_refs() {
let root: RootSchema = serde_json::from_value(json!({
"allOf": [{"$ref": "#/definitions/loop"}],
"properties": {"nested": {"properties": {
"sources": {"additionalProperties": {"type": "object"}}
}}},
"definitions": {"loop": {"$ref": "#/definitions/loop"}}
}))
.unwrap();
assert!(root.root_map_value_schema("sources").is_none());
}
}
52 changes: 52 additions & 0 deletions lib/vector-config/src/schema/parser/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,24 @@ impl SchemaQuerier {
pub fn query(&self) -> SchemaQueryBuilder<'_> {
SchemaQueryBuilder::from_schema(&self.schema)
}

/// Gets the schema for values in a map at the configuration root.
pub fn root_map_value_schema(&self, property: &str) -> Result<SimpleSchema<'_>, QueryError> {
let value = self
.schema
.root_map_value_schema(property)
.ok_or(QueryError::NoMatches)?;
let schema = value.as_object().ok_or(QueryError::NoMatches)?;
let schema = match schema.reference.as_deref() {
Some(reference) => reference
.strip_prefix("#/definitions/")
.and_then(|name| self.schema.definitions.get(name))
.and_then(Schema::as_object)
.ok_or(QueryError::NoMatches)?,
None => schema,
};
Ok(schema.into())
}
}

/// A query builder for querying against a root schema.
Expand Down Expand Up @@ -430,3 +448,37 @@ fn schema_to_simple_schema(schema: &Schema) -> SimpleSchema<'_> {
schema: schema_object,
}
}

#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;

#[test]
fn queries_component_base_through_the_root_map() {
let querier = SchemaQuerier {
schema: serde_json::from_value(json!({
"allOf": [{"properties": {"sources": {
"additionalProperties": {"$ref": "#/definitions/outer"}
}}}],
"definitions": {"outer": {"type": "object", "properties": {
"shared": {"type": "boolean"}
}}}
}))
.unwrap(),
};
let base = querier.root_map_value_schema("sources").unwrap();
assert!(
base.into_inner()
.object
.as_ref()
.unwrap()
.properties
.contains_key("shared")
);
assert!(matches!(
querier.root_map_value_schema("sinks"),
Err(QueryError::NoMatches)
));
}
}
54 changes: 48 additions & 6 deletions lib/vector-config/src/schema/visitors/inline_single.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ impl Visitor for InlineSingleUseReferencesVisitor {
occurrence_visitor.visit_root_schema(root);
let occurrence_map = occurrence_visitor.occurrence_map;

// The root component maps identify their outer schemas. Keep those
// definitions available to docs consumers even when referenced once.
let component_bases: HashSet<&str> = ["sources", "transforms", "sinks"]
.into_iter()
.filter_map(|property| root.root_map_value_schema(property))
.filter_map(Schema::as_object)
.filter_map(|schema| schema.reference.as_deref())
.map(get_cleaned_schema_reference)
.collect();

self.eligible_to_inline = occurrence_map
.into_iter()
// Filter out any schemas which have more than one occurrence, as naturally, we're
Expand All @@ -60,7 +70,8 @@ impl Visitor for InlineSingleUseReferencesVisitor {
.and_then(Schema::as_object)
.expect("schema definition must exist");

is_inlineable_schema(def_name.as_ref(), schema)
!component_bases.contains(def_name.as_ref())
&& is_inlineable_schema(def_name.as_ref(), schema)
})
.map(|s| s.as_ref().to_string())
.collect::<HashSet<_>>();
Expand Down Expand Up @@ -122,19 +133,18 @@ fn is_inlineable_schema(definition_name: &str, schema: &SchemaObject) -> bool {
"vector::sinks::Sinks",
];

// We want to avoid inlining all of the relevant top-level types used for defining components:
// the "outer" types (i.e. `SinkOuter<T>`), the enum/collection types (i.e. the big `Sources`
// enum), and the component configuration types themselves (i.e. `AmqpSinkConfig`).
// Outer types are protected through the root component maps above. Also keep
// enum/collection types (the big `Sources` enum) and individual component
// configuration types (such as `AmqpSinkConfig`).
//
// There's nothing _technically_ wrong with doing so, but it would break downstream consumers of
// the schema that parse it in order to extract the individual components and other
// component-specific metadata.
let is_component_base = get_schema_metadata_attr(schema, "docs::component_base_type").is_some();
let is_component = get_schema_metadata_attr(schema, "docs::component_type").is_some();

let is_allowed_schema = !DISALLOWED_SCHEMAS.contains(&definition_name);

!is_component_base && !is_component && is_allowed_schema
!is_component && is_allowed_schema
}

#[derive(Debug, Default)]
Expand Down Expand Up @@ -207,6 +217,38 @@ mod tests {
assert_schemas_eq(expected_schema, actual_schema);
}

#[test]
fn retains_root_component_map_values_without_metadata() {
for property in ["sources", "sinks", "transforms"] {
let mut schema = as_schema(json!({
"allOf": [{"$ref": "#/definitions/config"}],
"definitions": {
"config": {"properties": {property: {"$ref": "#/definitions/map"}}},
"map": {"type": "object", "additionalProperties": {"$ref": "#/definitions/renamed_outer"}},
"renamed_outer": {"type": "object", "properties": {
"shared": {"$ref": "#/definitions/inline_me"}
}},
"inline_me": {"type": "string"}
}
}));
InlineSingleUseReferencesVisitor::default().visit_root_schema(&mut schema);
assert!(schema.definitions.contains_key("renamed_outer"));
assert!(!schema.definitions.contains_key("map"));
assert!(!schema.definitions.contains_key("inline_me"));
assert_eq!(schema.definitions.len(), 1);
assert_eq!(
schema
.root_map_value_schema(property)
.unwrap()
.as_object()
.unwrap()
.reference
.as_deref(),
Some("#/definitions/renamed_outer")
);
}
}

#[test]
fn single_ref_single_usage() {
let mut actual_schema = as_schema(json!({
Expand Down
1 change: 0 additions & 1 deletion src/config/sink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ impl<T: SinkConfig + 'static> From<T> for BoxedSink {

/// Fully resolved sink component.
#[configurable_component]
#[configurable(metadata(docs::component_base_type = "sink"))]
#[derive(Clone, derive_more::Debug)]
pub struct SinkOuter<T>
where
Expand Down
1 change: 0 additions & 1 deletion src/config/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ impl<T: SourceConfig + 'static> From<T> for BoxedSource {

/// Fully resolved source component.
#[configurable_component]
#[configurable(metadata(docs::component_base_type = "source"))]
#[derive(Clone, Debug)]
pub struct SourceOuter {
#[serde(default, skip_serializing_if = "vector_lib::serde::is_default")]
Expand Down
1 change: 0 additions & 1 deletion src/config/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ impl<T: TransformConfig + 'static> From<T> for BoxedTransform {

/// Fully resolved transform component.
#[configurable_component]
#[configurable(metadata(docs::component_base_type = "transform"))]
#[derive(Clone, Debug)]
pub struct TransformOuter<T>
where
Expand Down
2 changes: 1 addition & 1 deletion vdev/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "vdev"
version = "0.3.20"
version = "0.3.21"
edition = "2024"
authors = ["Vector Contributors <vector@datadoghq.com>"]
license = "MPL-2.0"
Expand Down
58 changes: 46 additions & 12 deletions vdev/src/commands/build/component_docs/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,18 +364,13 @@ pub fn run(schema_path: &Path) -> Result<()> {
let component_types = ["source", "transform", "sink"];

// 1. Process Component Bases (sorted by component type for deterministic output)
let mut component_bases: IndexMap<String, String> = IndexMap::new();
if let Some(definitions) = root_schema.get("definitions").and_then(|d| d.as_object()) {
for (key, definition) in definitions {
if let Some(base_type) =
super::schema::get_schema_metadata(definition, "docs::component_base_type")
.and_then(|v| v.as_str())
&& component_types.contains(&base_type)
{
component_bases.insert(base_type.to_string(), key.clone());
}
}
}
let mut component_bases: IndexMap<String, String> = component_types
.iter()
.map(|kind| {
let name = component_base_schema_name(&context, kind)?;
Ok(((*kind).to_owned(), name.to_owned()))
})
.collect::<Result<_>>()?;
component_bases.sort_keys();

for (comp_type, schema_name) in &component_bases {
Expand Down Expand Up @@ -554,6 +549,18 @@ fn render_schema(
}
}

// Component maps already point at their outer configuration schemas. Use that
// relationship rather than requiring a second identifier on each definition.
fn component_base_schema_name<'a>(context: &'a SchemaContext, kind: &str) -> Result<&'a str> {
let field = format!("{kind}s");
context
.find_nested_object_property_schema(&context.root_schema, &field)
.and_then(|schema| schema.get("additionalProperties"))
.and_then(super::schema::get_schema_ref)
.and_then(|reference| reference.strip_prefix("#/definitions/"))
.with_context(|| format!("Could not find component base reference for '{field}'"))
}

fn render_generated_component_schema(
context: &mut SchemaContext,
schema_name: &str,
Expand Down Expand Up @@ -820,6 +827,33 @@ fn hide_cue_definitions_field(cue_output: &str) -> Result<String> {
mod tests {
use super::*;

#[test]
fn component_bases_come_from_root_map_references() {
let context = SchemaContext {
root_schema: json!({"allOf": [{"properties": {
"sources": {"additionalProperties": {"$ref": "#/definitions/renamed_source"}},
"sinks": {"additionalProperties": {"$ref": "#/definitions/generic_sink<String>"}},
"transforms": {"additionalProperties": {"$ref": "#/definitions/transform"}}
}}]}),
cue_binary_path: String::new(),
resolved_schema_cache: IndexMap::new(),
expanded_schema_cache: IndexMap::new(),
};
assert_eq!(
component_base_schema_name(&context, "source").unwrap(),
"renamed_source"
);
assert_eq!(
component_base_schema_name(&context, "sink").unwrap(),
"generic_sink<String>"
);
assert_eq!(
component_base_schema_name(&context, "transform").unwrap(),
"transform"
);
assert!(component_base_schema_name(&context, "missing").is_err());
}

#[test]
fn replaces_repeated_values_with_cue_references() {
let shared_name = "shared_options".to_string();
Expand Down
Loading