Skip to main content

swf_core/models/
schema.rs

1use crate::models::resource::*;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5/// Provides the default schema format
6fn default_schema_format() -> String {
7    SchemaFormat::JSON.to_string()
8}
9
10/// Enumerates all supported schema formats
11pub struct SchemaFormat;
12impl SchemaFormat {
13    /// Gets the Avro schema format
14    pub const AVRO: &'static str = "avro";
15    /// Gets the JSON schema format
16    pub const JSON: &'static str = "json";
17    /// Gets the XML schema format
18    pub const XML: &'static str = "xml";
19}
20
21/// Represents the definition of a schema
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct SchemaDefinition {
24    /// Gets/sets the schema's format. Defaults to 'json'. The (optional) version of the format can be set using `{format}:{version}`.
25    #[serde(default = "default_schema_format")]
26    pub format: String,
27
28    /// Gets/sets the schema's external resource, if any. Required if `document` has not been set.
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub resource: Option<ExternalResourceDefinition>,
31
32    /// Gets/sets the inline definition of the schema to use. Required if `resource` has not been set.
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub document: Option<Value>,
35}
36
37impl Default for SchemaDefinition {
38    fn default() -> Self {
39        SchemaDefinition {
40            format: default_schema_format(),
41            resource: None,
42            document: None,
43        }
44    }
45}
46
47impl SchemaDefinition {
48    /// Creates a new schema definition with an inline document
49    pub fn with_document(format: &str, document: Value) -> Self {
50        Self {
51            format: format.to_string(),
52            resource: None,
53            document: Some(document),
54        }
55    }
56
57    /// Creates a new schema definition with an external resource
58    pub fn with_resource(format: &str, resource: ExternalResourceDefinition) -> Self {
59        Self {
60            format: format.to_string(),
61            resource: Some(resource),
62            document: None,
63        }
64    }
65
66    /// Validates that the schema definition is valid (document XOR resource)
67    pub fn validate(&self) -> Result<(), SchemaValidationError> {
68        match (&self.document, &self.resource) {
69            (Some(_), Some(_)) => Err(SchemaValidationError::BothSet),
70            (None, None) => Err(SchemaValidationError::NeitherSet),
71            _ => Ok(()),
72        }
73    }
74
75    /// Gets whether this schema uses an inline document
76    pub fn is_document(&self) -> bool {
77        self.document.is_some() && self.resource.is_none()
78    }
79
80    /// Gets whether this schema uses an external resource
81    pub fn is_resource(&self) -> bool {
82        self.resource.is_some() && self.document.is_none()
83    }
84}
85
86/// Represents validation errors for schema definitions
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum SchemaValidationError {
89    /// Both document and resource are set (should be mutually exclusive)
90    BothSet,
91    /// Neither document nor resource is set (one is required)
92    NeitherSet,
93}
94
95impl std::fmt::Display for SchemaValidationError {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        match self {
98            SchemaValidationError::BothSet => write!(
99                f,
100                "Schema 'document' and 'resource' are mutually exclusive; only one can be set"
101            ),
102            SchemaValidationError::NeitherSet => {
103                write!(f, "Schema must have either 'document' or 'resource' set")
104            }
105        }
106    }
107}
108
109impl std::error::Error for SchemaValidationError {}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_schema_default_format() {
117        let schema = SchemaDefinition::default();
118        assert_eq!(schema.format, "json");
119    }
120
121    #[test]
122    fn test_schema_with_document() {
123        let doc = serde_json::json!({"type": "object", "properties": {"key": {"type": "string"}}});
124        let schema = SchemaDefinition::with_document("json", doc.clone());
125        assert!(schema.is_document());
126        assert!(!schema.is_resource());
127        assert_eq!(schema.document, Some(doc));
128    }
129
130    #[test]
131    fn test_schema_with_resource() {
132        let resource = ExternalResourceDefinition {
133            name: Some("mySchema".to_string()),
134            endpoint: crate::models::resource::OneOfEndpointDefinitionOrUri::Uri(
135                "http://example.com/schema".to_string(),
136            ),
137        };
138        let schema = SchemaDefinition::with_resource("json", resource);
139        assert!(schema.is_resource());
140        assert!(!schema.is_document());
141    }
142
143    #[test]
144    fn test_schema_validate_document() {
145        let schema = SchemaDefinition::with_document("json", serde_json::json!({}));
146        assert!(schema.validate().is_ok());
147    }
148
149    #[test]
150    fn test_schema_validate_neither() {
151        let schema = SchemaDefinition::default();
152        assert!(matches!(
153            schema.validate(),
154            Err(SchemaValidationError::NeitherSet)
155        ));
156    }
157
158    #[test]
159    fn test_schema_deserialize_with_document() {
160        let json = r#"{
161            "format": "json",
162            "document": {"type": "object"}
163        }"#;
164        let schema: SchemaDefinition = serde_json::from_str(json).unwrap();
165        assert_eq!(schema.format, "json");
166        assert!(schema.document.is_some());
167    }
168
169    #[test]
170    fn test_schema_deserialize_with_resource() {
171        let json = r#"{
172            "format": "avro",
173            "resource": {
174                "name": "myAvroSchema",
175                "endpoint": "http://example.com/schema.avsc"
176            }
177        }"#;
178        let schema: SchemaDefinition = serde_json::from_str(json).unwrap();
179        assert_eq!(schema.format, "avro");
180        assert!(schema.resource.is_some());
181    }
182
183    #[test]
184    fn test_schema_roundtrip() {
185        let json = r#"{"format": "json", "document": {"type": "object"}}"#;
186        let schema: SchemaDefinition = serde_json::from_str(json).unwrap();
187        let serialized = serde_json::to_string(&schema).unwrap();
188        let deserialized: SchemaDefinition = serde_json::from_str(&serialized).unwrap();
189        assert_eq!(schema, deserialized);
190    }
191
192    // Additional tests matching Go SDK patterns
193
194    #[test]
195    fn test_schema_default_format_on_deserialize() {
196        // When format is omitted, it should default to "json"
197        let json = r#"{"document": {"type": "object"}}"#;
198        let schema: SchemaDefinition = serde_json::from_str(json).unwrap();
199        assert_eq!(schema.format, "json");
200    }
201
202    #[test]
203    fn test_schema_format_with_version() {
204        // Format can include version: "json:2020-12"
205        let json = r#"{"format": "json:2020-12", "document": {"type": "object"}}"#;
206        let schema: SchemaDefinition = serde_json::from_str(json).unwrap();
207        assert_eq!(schema.format, "json:2020-12");
208    }
209
210    #[test]
211    fn test_schema_avro_format() {
212        let json = r#"{"format": "avro", "document": {"type": "record", "name": "Test"}}"#;
213        let schema: SchemaDefinition = serde_json::from_str(json).unwrap();
214        assert_eq!(schema.format, "avro");
215    }
216
217    #[test]
218    fn test_schema_validate_both_set() {
219        // Both document and resource set should be invalid
220        let mut schema = SchemaDefinition::with_document("json", serde_json::json!({}));
221        schema.resource = Some(ExternalResourceDefinition {
222            name: Some("test".to_string()),
223            endpoint: crate::models::resource::OneOfEndpointDefinitionOrUri::Uri(
224                "http://example.com".to_string(),
225            ),
226        });
227        assert!(matches!(
228            schema.validate(),
229            Err(SchemaValidationError::BothSet)
230        ));
231    }
232
233    #[test]
234    fn test_schema_with_resource_roundtrip() {
235        let json = r#"{
236            "format": "json",
237            "resource": {
238                "name": "mySchema",
239                "endpoint": "http://example.com/schema.json"
240            }
241        }"#;
242        let schema: SchemaDefinition = serde_json::from_str(json).unwrap();
243        let serialized = serde_json::to_string(&schema).unwrap();
244        let deserialized: SchemaDefinition = serde_json::from_str(&serialized).unwrap();
245        assert_eq!(schema, deserialized);
246    }
247
248    #[test]
249    fn test_schema_validation_error_display() {
250        let both = SchemaValidationError::BothSet;
251        assert!(both.to_string().contains("mutually exclusive"));
252
253        let neither = SchemaValidationError::NeitherSet;
254        assert!(neither.to_string().contains("must have either"));
255    }
256}