Skip to main content

kinetic_doc_types/
lib.rs

1//! Shared metadata types for Kinetic's documentation generation pipeline.
2//!
3//! These types are referenced by:
4//! - The `kinetic-doc-derive` proc-macro (generated code uses these types)
5//! - The `doc-gen` tool (reads/writes `ComponentMetadata` as JSON)
6//! - Any crate that derives `ComponentDoc`
7
8use serde::{Deserialize, Serialize};
9
10/// Metadata for a single component (source, transform, or sink).
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12pub struct ComponentMetadata {
13    pub component_type: String,
14    pub name: String,
15    pub status: String,
16    pub description: String,
17    pub fields: Vec<FieldMetadata>,
18    /// Component-specific metrics (beyond standard component metrics).
19    #[serde(default, skip_serializing_if = "Vec::is_empty")]
20    pub metrics: Vec<MetricMetadata>,
21    /// Output schema fields (for sources).
22    #[serde(default, skip_serializing_if = "Vec::is_empty")]
23    pub outputs: Vec<OutputFieldMetadata>,
24    /// Environment variables this component reads.
25    #[serde(default, skip_serializing_if = "Vec::is_empty")]
26    pub env_vars: Vec<EnvVarMetadata>,
27    /// Required IAM permissions (for AWS/cloud components).
28    #[serde(default, skip_serializing_if = "Vec::is_empty")]
29    pub permissions: Vec<PermissionMetadata>,
30}
31
32/// Metadata for a single configuration field.
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34pub struct FieldMetadata {
35    pub name: String,
36    pub rust_type: String,
37    pub user_type: String,
38    pub required: bool,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub default: Option<String>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub example: Option<String>,
43    pub secret: bool,
44    pub description: String,
45    /// Nested children for flattened structs or enum variants.
46    #[serde(default, skip_serializing_if = "Vec::is_empty")]
47    pub children: Vec<FieldMetadata>,
48    /// Category/group for sub-section rendering.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub category: Option<String>,
51    /// Enum values if this field is an enum type.
52    #[serde(default, skip_serializing_if = "Vec::is_empty")]
53    pub enum_values: Vec<EnumValueMetadata>,
54    /// For polymorphic enum fields (like SamplingMode), the variant schemas.
55    #[serde(default, skip_serializing_if = "Vec::is_empty")]
56    pub variants: Vec<VariantMetadata>,
57    /// Cross-reference link to another component's documentation.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub reference: Option<String>,
60    /// Type of reference for categorization (e.g., "codec", "component", "transform").
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub reference_type: Option<String>,
63}
64
65/// Metadata for an enum variant.
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67pub struct EnumValueMetadata {
68    /// The variant name (snake_case for serde).
69    pub name: String,
70    pub description: String,
71}
72
73/// Metadata for a polymorphic enum variant (e.g., Random, Systematic in SamplingMode).
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
75pub struct VariantMetadata {
76    /// Variant name (snake_case).
77    pub name: String,
78    pub description: String,
79    /// Fields specific to this variant.
80    pub fields: Vec<FieldMetadata>,
81    /// Example YAML snippet for this variant.
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub example: Option<String>,
84}
85
86/// Metadata for a component-specific metric.
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
88pub struct MetricMetadata {
89    pub name: String,
90    pub metric_type: String, // "counter", "histogram", "gauge"
91    pub description: String,
92    /// Dimension/tag names for this metric.
93    #[serde(default, skip_serializing_if = "Vec::is_empty")]
94    pub tags: Vec<String>,
95}
96
97/// Metadata for an output field (sources only).
98#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
99pub struct OutputFieldMetadata {
100    pub name: String,
101    pub field_type: String,
102    pub description: String,
103}
104
105/// Metadata for an environment variable.
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
107pub struct EnvVarMetadata {
108    pub name: String,
109    pub description: String,
110}
111
112/// Metadata for a required IAM permission.
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
114pub struct PermissionMetadata {
115    pub action: String,
116    pub service: String,
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub notes: Option<String>,
119}
120
121/// Trait for types that can provide metadata about their fields.
122pub trait HasFieldsMetadata {
123    /// Returns a list of metadata for all fields in the struct.
124    fn fields_metadata() -> Vec<FieldMetadata>;
125}
126
127/// Implement `HasFieldsMetadata` for `Option<T>` to support flattening optional structs.
128impl<T: HasFieldsMetadata> HasFieldsMetadata for Option<T> {
129    fn fields_metadata() -> Vec<FieldMetadata> {
130        T::fields_metadata()
131    }
132}
133
134/// Implement `HasFieldsMetadata` for `Box<T>` to support flattening boxed structs.
135impl<T: HasFieldsMetadata> HasFieldsMetadata for Box<T> {
136    fn fields_metadata() -> Vec<FieldMetadata> {
137        T::fields_metadata()
138    }
139}
140
141/// Marker trait for enums to provide metadata about their variants.
142pub trait HasEnumMetadata {
143    /// Returns a list of metadata for all variants in the enum.
144    fn enum_metadata() -> Vec<String>;
145}
146
147/// Trait for polymorphic enums to provide variant schema metadata.
148pub trait HasVariantMetadata {
149    /// Returns a list of variant metadata including field schemas.
150    fn variant_metadata() -> Vec<VariantMetadata>;
151}
152
153/// Collection of all component metadata for the intermediate representation.
154#[derive(Debug, Clone, Serialize, Deserialize, Default)]
155pub struct ComponentRegistry {
156    pub components: Vec<ComponentMetadata>,
157}
158
159impl ComponentRegistry {
160    /// Sorts components by type then name for deterministic output.
161    pub fn sort(&mut self) {
162        self.components
163            .sort_by(|a, b| (&a.component_type, &a.name).cmp(&(&b.component_type, &b.name)));
164    }
165
166    /// Serializes the registry to pretty-printed JSON.
167    ///
168    /// # Errors
169    ///
170    /// Returns an error if serialization fails.
171    pub fn to_json(&self) -> serde_json::Result<String> {
172        serde_json::to_string_pretty(self)
173    }
174
175    /// Deserializes a registry from JSON.
176    ///
177    /// # Errors
178    ///
179    /// Returns an error if deserialization fails.
180    pub fn from_json(json: &str) -> serde_json::Result<Self> {
181        serde_json::from_str(json)
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_roundtrip_json() {
191        let registry = ComponentRegistry {
192            components: vec![ComponentMetadata {
193                component_type: "sink".to_string(),
194                name: "kafka".to_string(),
195                status: "stable".to_string(),
196                description: "Produces events to Kafka.".to_string(),
197                fields: vec![FieldMetadata {
198                    name: "topic".to_string(),
199                    rust_type: "String".to_string(),
200                    user_type: "string".to_string(),
201                    required: true,
202                    default: None,
203                    example: Some("events".to_string()),
204                    secret: false,
205                    description: "The Kafka topic.".to_string(),
206                    children: vec![],
207                    category: None,
208                    enum_values: vec![],
209                    variants: vec![],
210                    reference: None,
211                    reference_type: None,
212                }],
213                metrics: vec![],
214                outputs: vec![],
215                env_vars: vec![],
216                permissions: vec![],
217            }],
218        };
219        let json = registry.to_json().unwrap();
220        let roundtripped = ComponentRegistry::from_json(&json).unwrap();
221        assert_eq!(registry.components, roundtripped.components);
222    }
223
224    #[test]
225    fn test_sort_deterministic() {
226        let mut registry = ComponentRegistry {
227            components: vec![
228                ComponentMetadata {
229                    component_type: "transform".to_string(),
230                    name: "filter".to_string(),
231                    status: "stable".to_string(),
232                    description: String::new(),
233                    fields: vec![],
234                    metrics: vec![],
235                    outputs: vec![],
236                    env_vars: vec![],
237                    permissions: vec![],
238                },
239                ComponentMetadata {
240                    component_type: "source".to_string(),
241                    name: "kafka".to_string(),
242                    status: "stable".to_string(),
243                    description: String::new(),
244                    fields: vec![],
245                    metrics: vec![],
246                    outputs: vec![],
247                    env_vars: vec![],
248                    permissions: vec![],
249                },
250                ComponentMetadata {
251                    component_type: "sink".to_string(),
252                    name: "s3".to_string(),
253                    status: "stable".to_string(),
254                    description: String::new(),
255                    fields: vec![],
256                    metrics: vec![],
257                    outputs: vec![],
258                    env_vars: vec![],
259                    permissions: vec![],
260                },
261            ],
262        };
263        registry.sort();
264        let types: Vec<&str> = registry
265            .components
266            .iter()
267            .map(|c| c.component_type.as_str())
268            .collect();
269        assert_eq!(types, vec!["sink", "source", "transform"]);
270    }
271}