1use serde::{Deserialize, Serialize};
8use serde_json::Value as JsonValue;
9use std::collections::HashMap;
10use std::path::Path;
11
12use crate::error::{CoreError, Result, ValidationErrorInfo};
13use crate::values::Values;
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
17#[serde(rename_all = "lowercase")]
18pub enum SherpType {
19 String,
20 Number,
21 Integer,
22 Boolean,
23 Array,
24 Object,
25 Any,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct SherpProperty {
32 #[serde(rename = "type")]
34 pub prop_type: SherpType,
35
36 #[serde(default)]
38 pub description: Option<String>,
39
40 #[serde(default)]
42 pub default: Option<JsonValue>,
43
44 #[serde(default)]
46 pub required: bool,
47
48 #[serde(default)]
50 pub enum_values: Option<Vec<JsonValue>>,
51
52 #[serde(default)]
54 pub pattern: Option<String>,
55
56 #[serde(default)]
58 pub min: Option<f64>,
59
60 #[serde(default)]
62 pub max: Option<f64>,
63
64 #[serde(default)]
66 pub min_length: Option<usize>,
67
68 #[serde(default)]
70 pub max_length: Option<usize>,
71
72 #[serde(default)]
74 pub properties: Option<HashMap<String, SherpProperty>>,
75
76 #[serde(default)]
78 pub items: Option<Box<SherpProperty>>,
79
80 #[serde(default)]
82 pub min_items: Option<usize>,
83
84 #[serde(default)]
86 pub max_items: Option<usize>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91#[serde(rename_all = "camelCase")]
92pub struct SherpSchema {
93 #[serde(default = "default_schema_version")]
95 pub schema_version: String,
96
97 #[serde(default)]
99 pub title: Option<String>,
100
101 #[serde(default)]
103 pub description: Option<String>,
104
105 pub properties: HashMap<String, SherpProperty>,
107}
108
109fn default_schema_version() -> String {
110 "sherpack/v1".to_string()
111}
112
113#[derive(Debug, Clone, Copy, PartialEq)]
115pub enum SchemaFormat {
116 JsonSchema,
117 SherpSchema,
118}
119
120#[derive(Debug, Clone)]
122pub enum Schema {
123 JsonSchema(JsonValue),
125 SherpSchema(SherpSchema),
127}
128
129impl Schema {
130 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
132 let path = path.as_ref();
133 let content = std::fs::read_to_string(path)?;
134
135 let format = detect_schema_format(path, &content)?;
137
138 match format {
139 SchemaFormat::JsonSchema => {
140 let value: JsonValue = if path.extension().map(|e| e == "json").unwrap_or(false) {
141 serde_json::from_str(&content)?
142 } else {
143 serde_yaml::from_str(&content)?
144 };
145 Ok(Schema::JsonSchema(value))
146 }
147 SchemaFormat::SherpSchema => {
148 let sherp: SherpSchema = serde_yaml::from_str(&content)?;
149 Ok(Schema::SherpSchema(sherp))
150 }
151 }
152 }
153
154 pub fn from_json_schema(json: &str) -> Result<Self> {
156 let value: JsonValue = serde_json::from_str(json)?;
157 Ok(Schema::JsonSchema(value))
158 }
159
160 pub fn from_sherp_schema(yaml: &str) -> Result<Self> {
162 let sherp: SherpSchema = serde_yaml::from_str(yaml)?;
163 Ok(Schema::SherpSchema(sherp))
164 }
165
166 pub fn to_json_schema(&self) -> JsonValue {
168 match self {
169 Schema::JsonSchema(v) => v.clone(),
170 Schema::SherpSchema(s) => convert_sherp_to_json_schema(s),
171 }
172 }
173
174 pub fn extract_defaults(&self) -> JsonValue {
176 match self {
177 Schema::JsonSchema(v) => extract_json_schema_defaults(v),
178 Schema::SherpSchema(s) => extract_sherp_defaults(s),
179 }
180 }
181
182 pub fn defaults_as_values(&self) -> Values {
184 Values(self.extract_defaults())
185 }
186}
187
188fn detect_schema_format(path: &Path, content: &str) -> Result<SchemaFormat> {
190 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
192 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
193
194 if name == "values.schema.json" || ext == "json" {
196 return Ok(SchemaFormat::JsonSchema);
197 }
198
199 let value: JsonValue = serde_yaml::from_str(content).map_err(|e| CoreError::InvalidSchema {
201 message: format!("Failed to parse schema: {}", e),
202 })?;
203
204 if let Some(obj) = value.as_object() {
205 if obj.contains_key("$schema") || obj.contains_key("$id") {
207 return Ok(SchemaFormat::JsonSchema);
208 }
209
210 if obj
212 .get("schemaVersion")
213 .and_then(|v| v.as_str())
214 .map(|s| s.starts_with("sherpack/"))
215 .unwrap_or(false)
216 {
217 return Ok(SchemaFormat::SherpSchema);
218 }
219
220 if obj.contains_key("type") && obj.get("type") == Some(&JsonValue::String("object".into()))
223 {
224 return Ok(SchemaFormat::JsonSchema);
225 }
226 }
227
228 Ok(SchemaFormat::SherpSchema)
230}
231
232fn convert_sherp_to_json_schema(sherp: &SherpSchema) -> JsonValue {
234 let mut schema = serde_json::Map::new();
235
236 schema.insert(
237 "$schema".into(),
238 JsonValue::String("http://json-schema.org/draft-07/schema#".into()),
239 );
240 schema.insert("type".into(), JsonValue::String("object".into()));
241
242 if let Some(title) = &sherp.title {
243 schema.insert("title".into(), JsonValue::String(title.clone()));
244 }
245 if let Some(desc) = &sherp.description {
246 schema.insert("description".into(), JsonValue::String(desc.clone()));
247 }
248
249 let (properties, required) = convert_sherp_properties(&sherp.properties);
250 schema.insert("properties".into(), properties);
251
252 if !required.is_empty() {
253 schema.insert(
254 "required".into(),
255 JsonValue::Array(required.into_iter().map(JsonValue::String).collect()),
256 );
257 }
258
259 JsonValue::Object(schema)
260}
261
262fn convert_sherp_properties(props: &HashMap<String, SherpProperty>) -> (JsonValue, Vec<String>) {
263 let mut json_props = serde_json::Map::new();
264 let mut required = Vec::new();
265
266 for (name, prop) in props {
267 json_props.insert(name.clone(), convert_sherp_property(prop));
268 if prop.required {
269 required.push(name.clone());
270 }
271 }
272
273 (JsonValue::Object(json_props), required)
274}
275
276fn convert_sherp_property(prop: &SherpProperty) -> JsonValue {
277 let mut json = serde_json::Map::new();
278
279 let type_str = match prop.prop_type {
281 SherpType::String => "string",
282 SherpType::Number => "number",
283 SherpType::Integer => "integer",
284 SherpType::Boolean => "boolean",
285 SherpType::Array => "array",
286 SherpType::Object => "object",
287 SherpType::Any => {
288 return JsonValue::Object(json);
290 }
291 };
292 json.insert("type".into(), JsonValue::String(type_str.into()));
293
294 if let Some(desc) = &prop.description {
296 json.insert("description".into(), JsonValue::String(desc.clone()));
297 }
298 if let Some(default) = &prop.default {
299 json.insert("default".into(), default.clone());
300 }
301 if let Some(enum_vals) = &prop.enum_values {
302 json.insert("enum".into(), JsonValue::Array(enum_vals.clone()));
303 }
304 if let Some(pattern) = &prop.pattern {
305 json.insert("pattern".into(), JsonValue::String(pattern.clone()));
306 }
307
308 if let Some(min) = prop.min {
310 json.insert("minimum".into(), JsonValue::from(min));
311 }
312 if let Some(max) = prop.max {
313 json.insert("maximum".into(), JsonValue::from(max));
314 }
315
316 if let Some(min_len) = prop.min_length {
318 json.insert("minLength".into(), JsonValue::from(min_len));
319 }
320 if let Some(max_len) = prop.max_length {
321 json.insert("maxLength".into(), JsonValue::from(max_len));
322 }
323
324 if let Some(nested_props) = &prop.properties {
326 let (nested_json, nested_required) = convert_sherp_properties(nested_props);
327 json.insert("properties".into(), nested_json);
328 if !nested_required.is_empty() {
329 json.insert(
330 "required".into(),
331 JsonValue::Array(nested_required.into_iter().map(JsonValue::String).collect()),
332 );
333 }
334 }
335
336 if let Some(items) = &prop.items {
338 json.insert("items".into(), convert_sherp_property(items));
339 }
340 if let Some(min_items) = prop.min_items {
341 json.insert("minItems".into(), JsonValue::from(min_items));
342 }
343 if let Some(max_items) = prop.max_items {
344 json.insert("maxItems".into(), JsonValue::from(max_items));
345 }
346
347 JsonValue::Object(json)
348}
349
350fn extract_json_schema_defaults(schema: &JsonValue) -> JsonValue {
352 extract_defaults_recursive(schema)
353}
354
355fn extract_defaults_recursive(schema: &JsonValue) -> JsonValue {
356 let obj = match schema.as_object() {
357 Some(o) => o,
358 None => return JsonValue::Null,
359 };
360
361 if let Some(default) = obj.get("default") {
363 return default.clone();
364 }
365
366 if obj.get("type") == Some(&JsonValue::String("object".into()))
368 && let Some(props) = obj.get("properties").and_then(|p| p.as_object())
369 {
370 let mut defaults = serde_json::Map::new();
371
372 for (key, prop_schema) in props {
373 let prop_default = extract_defaults_recursive(prop_schema);
374 if !prop_default.is_null() {
375 defaults.insert(key.clone(), prop_default);
376 }
377 }
378
379 if !defaults.is_empty() {
380 return JsonValue::Object(defaults);
381 }
382 }
383
384 JsonValue::Null
385}
386
387fn extract_sherp_defaults(sherp: &SherpSchema) -> JsonValue {
389 extract_sherp_property_defaults(&sherp.properties)
390}
391
392fn extract_sherp_property_defaults(props: &HashMap<String, SherpProperty>) -> JsonValue {
393 let mut defaults = serde_json::Map::new();
394
395 for (name, prop) in props {
396 let value = if let Some(default) = &prop.default {
397 default.clone()
398 } else if let Some(nested) = &prop.properties {
399 let nested_defaults = extract_sherp_property_defaults(nested);
400 if nested_defaults.is_null()
401 || nested_defaults
402 .as_object()
403 .map(|o| o.is_empty())
404 .unwrap_or(true)
405 {
406 continue;
407 }
408 nested_defaults
409 } else {
410 continue;
411 };
412
413 defaults.insert(name.clone(), value);
414 }
415
416 if defaults.is_empty() {
417 JsonValue::Null
418 } else {
419 JsonValue::Object(defaults)
420 }
421}
422
423#[derive(Debug)]
425pub struct ValidationResult {
426 pub is_valid: bool,
428 pub errors: Vec<ValidationErrorInfo>,
430}
431
432impl ValidationResult {
433 pub fn success() -> Self {
435 Self {
436 is_valid: true,
437 errors: vec![],
438 }
439 }
440
441 pub fn failure(errors: Vec<ValidationErrorInfo>) -> Self {
443 Self {
444 is_valid: false,
445 errors,
446 }
447 }
448}
449
450pub struct SchemaValidator {
452 schema: Schema,
454
455 compiled: jsonschema::Validator,
457
458 defaults: JsonValue,
460}
461
462impl SchemaValidator {
463 pub fn new(schema: Schema) -> Result<Self> {
465 let json_schema = schema.to_json_schema();
466 let defaults = schema.extract_defaults();
467
468 let compiled =
469 jsonschema::validator_for(&json_schema).map_err(|e| CoreError::InvalidSchema {
470 message: format!("Invalid schema: {}", e),
471 })?;
472
473 Ok(Self {
474 schema,
475 compiled,
476 defaults,
477 })
478 }
479
480 pub fn validate(&self, values: &JsonValue) -> ValidationResult {
482 if self.compiled.is_valid(values) {
483 return ValidationResult::success();
484 }
485
486 let errors: Vec<ValidationErrorInfo> = self
488 .compiled
489 .iter_errors(values)
490 .map(|e| {
491 let path = e.instance_path().to_string();
492 ValidationErrorInfo {
493 path: if path.is_empty() {
494 "(root)".to_string()
495 } else {
496 path
497 },
498 message: format_validation_error(&e),
499 expected: None,
500 actual: None,
501 }
502 })
503 .collect();
504
505 ValidationResult::failure(errors)
506 }
507
508 pub fn defaults(&self) -> &JsonValue {
510 &self.defaults
511 }
512
513 pub fn defaults_as_values(&self) -> Values {
515 Values(self.defaults.clone())
516 }
517
518 pub fn schema(&self) -> &Schema {
520 &self.schema
521 }
522}
523
524fn format_validation_error(error: &jsonschema::ValidationError) -> String {
526 let msg = error.to_string();
527
528 msg.replace("\"", "'")
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535
536 #[test]
537 fn test_sherp_schema_parse() {
538 let yaml = r#"
539schemaVersion: sherpack/v1
540title: Test Schema
541properties:
542 app:
543 type: object
544 properties:
545 name:
546 type: string
547 required: true
548 replicas:
549 type: integer
550 default: 1
551 min: 0
552 max: 100
553"#;
554
555 let schema = Schema::from_sherp_schema(yaml).unwrap();
556 match schema {
557 Schema::SherpSchema(s) => {
558 assert_eq!(s.title, Some("Test Schema".to_string()));
559 assert!(s.properties.contains_key("app"));
560 }
561 _ => panic!("Expected SherpSchema"),
562 }
563 }
564
565 #[test]
566 fn test_sherp_to_json_schema_conversion() {
567 let yaml = r#"
568schemaVersion: sherpack/v1
569properties:
570 name:
571 type: string
572 required: true
573 replicas:
574 type: integer
575 default: 3
576"#;
577
578 let schema = Schema::from_sherp_schema(yaml).unwrap();
579 let json_schema = schema.to_json_schema();
580
581 let obj = json_schema.as_object().unwrap();
582 assert_eq!(obj.get("type"), Some(&JsonValue::String("object".into())));
583 assert!(obj.contains_key("properties"));
584
585 let required = obj.get("required").unwrap().as_array().unwrap();
586 assert!(required.contains(&JsonValue::String("name".into())));
587 }
588
589 #[test]
590 fn test_extract_defaults() {
591 let yaml = r#"
592schemaVersion: sherpack/v1
593properties:
594 replicas:
595 type: integer
596 default: 3
597 image:
598 type: object
599 properties:
600 tag:
601 type: string
602 default: latest
603 pullPolicy:
604 type: string
605 default: IfNotPresent
606"#;
607
608 let schema = Schema::from_sherp_schema(yaml).unwrap();
609 let defaults = schema.extract_defaults();
610
611 assert_eq!(defaults.get("replicas"), Some(&JsonValue::from(3)));
612
613 let image = defaults.get("image").unwrap();
614 assert_eq!(image.get("tag"), Some(&JsonValue::String("latest".into())));
615 assert_eq!(
616 image.get("pullPolicy"),
617 Some(&JsonValue::String("IfNotPresent".into()))
618 );
619 }
620
621 #[test]
622 fn test_validation_success() {
623 let yaml = r#"
624schemaVersion: sherpack/v1
625properties:
626 replicas:
627 type: integer
628 min: 0
629 max: 10
630"#;
631
632 let schema = Schema::from_sherp_schema(yaml).unwrap();
633 let validator = SchemaValidator::new(schema).unwrap();
634
635 let values = serde_json::json!({
636 "replicas": 5
637 });
638
639 let result = validator.validate(&values);
640 assert!(result.is_valid);
641 assert!(result.errors.is_empty());
642 }
643
644 #[test]
645 fn test_validation_failure() {
646 let yaml = r#"
647schemaVersion: sherpack/v1
648properties:
649 replicas:
650 type: integer
651 min: 0
652 max: 10
653"#;
654
655 let schema = Schema::from_sherp_schema(yaml).unwrap();
656 let validator = SchemaValidator::new(schema).unwrap();
657
658 let values = serde_json::json!({
659 "replicas": "not a number"
660 });
661
662 let result = validator.validate(&values);
663 assert!(!result.is_valid);
664 assert!(!result.errors.is_empty());
665 }
666
667 #[test]
668 fn test_json_schema_detection() {
669 let json_schema = r#"{
670 "$schema": "http://json-schema.org/draft-07/schema#",
671 "type": "object",
672 "properties": {
673 "name": { "type": "string" }
674 }
675 }"#;
676
677 let schema = Schema::from_json_schema(json_schema).unwrap();
678 match schema {
679 Schema::JsonSchema(_) => {}
680 _ => panic!("Expected JsonSchema"),
681 }
682 }
683}