Skip to main content

regorus/
schema.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4#![allow(clippy::pattern_type_mismatch)]
5
6/// There are two type systems of interest:
7///     1. JSON Schema used by Azure Policy for some of its metadata.
8///     2. Bicep's type system generated from Azure API swagger files.
9///        https://github.com/Azure/bicep-types/blob/main/src/Bicep.Types/ConcreteTypes
10///
11/// JSON Schema is standardized and well documented, with good tooling support.
12/// JSON Schema is quite flexible. The following schema:
13/// {
14///   "allOf": [
15///     {
16///       "properties": {
17///         "name": {"type": "string" }
18///       },
19///       "required": ["name"]
20///     },
21///     {
22///       "properties": {
23///         "age": {"type": "integer" }
24///       },
25///       "required": ["age"]
26///     },
27///     {
28///       "minLength": 5
29///     }
30///   ]
31/// }
32///
33/// expresses the constraint that if a value happens to be an object, then it must have a string field `name`,
34/// and also an integer field 'age'. If it happens to be a string, it must have a minimum length of 5.
35/// There are also different ways to express the same contraint.
36///
37/// Such flexibility is not needed for our use cases as shown by Bicep's type system which only allows a subset of
38/// the constraints expressible in JSON Schema yet represents Azure Resources. Note that Bicep's type system models
39/// some JSON schema concepts such as `oneOf` differently.
40///
41/// For Regorus' type system, we will use a subset of JSON Schema that is needed to support Azure Policy.
42/// This subset is initially derived from the Bicep type system, but has a few other JSON Schema concepts like
43/// `enum`, `const` that are needed for Azure Policy. Additional JSON Schema features will be supported as needed.
44/// This approach is consistent with Azure Policy's use of JSON Schema for metadata.
45/// We can also potentially reuse the type schemas  (https://github.com/Azure/bicep-types-az)
46/// that the Bicep team generates from Azure API swagger files, using a custom deserializer to convert them to our type system.
47///
48/// Here is the mapping between Bicep's type system and JSON Schema:
49///
50/// AnyType
51/// Bicep:      { "$type": "AnyType" }
52/// JSON Schema: {}
53///
54/// BooleanType
55/// Bicep:      { "$type": "BooleanType" }
56/// JSON Schema: { "type": "boolean" }
57///
58/// NullType
59/// Bicep:      { "$type": "NullType" }
60/// JSON Schema: { "type": "null" }
61///
62/// IntegerType
63/// Bicep:      { "$type": "IntegerType", "minValue": X, "maxValue": Y }
64/// JSON Schema: { "type": "integer", "minimum": X, "maximum": Y }
65///
66/// NumberType (no Bicep equivalent)
67/// Bicep:      No equivalent
68/// JSON Schema: { "type": "number", "minimum": X, "maximum": Y }
69///
70/// StringType
71/// Bicep:      {
72///               "$type": "StringType",
73///               "minLength": X,
74///               "maxLength": Y,
75//               "pattern": "..."
76///             }
77/// JSON Schema: {
78///               "type": "string",
79///               "minLength": X,
80///               "maxLength": Y,
81///               "pattern": "..."
82///             }
83///
84/// Integer Constant (no Bicep equivalent)
85/// Bicep:      No equivalent
86/// JSON Schema: { "const": 5 }
87///
88/// UnionType
89/// Bicep:      { "$type": "UnionType", "elements": [...] }
90/// JSON Schema: { "enum": [...] } or { "anyOf": [...] }
91///
92/// Enum with inline values (no Bicep equivalent)
93/// Bicep:      No equivalent
94/// JSON Schema: { "enum": [8, 10] }
95///
96/// ObjectType
97/// Bicep:      {
98///               "$type": "ObjectType",
99///               "name": "Test.Rp1/testType1",
100///               "properties": {
101///                 "id": {
102///                   "type": { "$ref": "#/2" },
103///                   "flags": 10,
104///                   "description": "The resource id"
105///                 }
106///               },
107///               "additionalProperties": { "$ref": "#/3" }
108///             }
109/// JSON Schema: {
110///               "type": "object",
111///               "properties": {
112///                 "id": {
113///                   "type": "integer",
114///                   "description": "The resource id"
115///                 }
116///               },
117///               "required": ["id"],
118///               "additionalProperties": { "type": "boolean" }
119///             }
120///
121/// DiscriminatedObjectType
122/// Bicep:      {
123///               "$type": "DiscriminatedObjectType",
124///               "name": "Microsoft.Security/settings",
125///               "discriminator": "kind",
126///               "baseProperties": {
127///                 "name": {
128///                   "type": { "$ref": "#/5" },
129///                   "flags": 9,
130///                   "description": "The resource name"
131///                 }
132///               },
133///               "elements": {
134///                 "ASubObject": { "$ref": "#/9" },
135///                 "BSubObject": { "$ref": "#/13" }
136///               }
137///             }
138/// JSON Schema: {
139///               "type": "object",
140///               "properties": {
141///                 "name": {
142///                   "type": "string",
143///                   "description": "The resource name"
144///                 },
145///                 "kind": {
146///                   "description": "The kind of the resource",
147///                   "enum": ["ASubObject", "BSubObject"]
148///                 }
149///               },
150///               "allOf": [
151///                 {
152///                   "if": {
153///                     "properties": {
154///                       "kind": { "const": "ASubObject" }
155///                     }
156///                   },
157///                   "then": {
158///                     "properties": {
159///                       "apropertyA": {
160///                         "type": "string",
161///                         "description": "Property A of ASubObject"
162///                       }
163///                     },
164///                     "required": ["apropertyA"]
165///                   }
166///                 },
167///                 {
168///                   "if": {
169///                     "properties": {
170///                       "kind": { "const": "BSubObject" }
171///                     }
172///                   },
173///                   "then": {
174///                     "properties": {
175///                       "bpropertyB": {
176///                         "type": "string",
177///                         "description": "Property B of BSubObject"
178///                       }
179///                     },
180///                     "required": ["bpropertyB"]
181///                   }
182///                 }
183///               ]
184///             }
185///
186/// The type system is implemented with the following principles:
187///     - Types are immutable and can be shared safely across threads, allowing parallel schema validation using the same type.
188///     - Any unsupported JSON Schema feature should raise an error during type creation. Otherwise, the user will not know whether
189///       parts of their schema are ignored or not.
190///     - Leverage serde as much as possible for serialization and deserialization, avoiding custom serialization logic.
191///
192/// We use a Rust enum to represent the type system, with each variant representing a different type. In each variant,
193/// we list the properties that are relevant to that type, using `Option<T>` for properties that are not required.
194/// `deny_unknown_fields` is used to ensure that any unsupported fields in the JSON Schema will raise an error during deserialization.
195/// Some properties like `description` are duplicated in each variant, since `deny_unknown_fields` cannot be used with `#[serde(flatten)]`
196/// which would have allowed us to refactor the common properties into a single struct to avoid duplication.
197use alloc::collections::BTreeMap;
198use serde::{Deserialize, Deserializer};
199
200use crate::{format, Box, Rc, Value, Vec};
201
202type String = Rc<str>;
203
204pub mod error;
205mod meta;
206pub mod validate;
207
208/// A schema represents a type definition that can be used for validation.
209///
210/// `Schema` is a lightweight wrapper around a [`Type`] that provides reference counting
211/// for efficient sharing and cloning. It serves as the primary interface for working
212/// with type definitions in the Regorus type system.
213///
214/// # Usage
215///
216/// Schemas are typically created by deserializing from JSON Schema format:
217///
218/// ```rust
219/// use serde_json::json;
220///
221/// // Create a schema from JSON
222/// let schema_json = json!({
223///     "type": "object",
224///     "properties": {
225///         "name": { "type": "string" },
226///         "age": { "type": "integer", "minimum": 0 }
227///     },
228///     "required": ["name"]
229/// });
230///
231/// let schema: Schema = serde_json::from_value(schema_json).unwrap();
232/// ```
233///
234/// # Supported Schema Features
235///
236/// The schema system supports a subset of JSON Schema features needed for Azure Policy
237/// and other use cases:
238///
239/// - **Basic types**: `any`, `null`, `boolean`, `integer`, `number`, `string`
240/// - **Complex types**: `array`, `set`, `object`
241/// - **Value constraints**: `enum`, `const`
242/// - **Composition**: `anyOf` for union types
243/// - **String constraints**: `minLength`, `maxLength`, `pattern`
244/// - **Numeric constraints**: `minimum`, `maximum`
245/// - **Array constraints**: `minItems`, `maxItems`
246/// - **Object features**: `properties`, `required`, `additionalProperties`
247/// - **Discriminated unions**: via `allOf` with conditional schemas
248/// # Thread Safety
249///
250/// While `Schema` itself is not `Send` or `Sync` due to the use of `Rc`, it can be
251/// safely shared within a single thread and cloned efficiently. For multi-threaded
252/// scenarios, consider wrapping in `Arc` if needed.
253///
254/// # Examples
255///
256/// ## Simple String Schema
257/// ```rust
258/// let schema = json!({ "type": "string", "minLength": 1 });
259/// let parsed: Schema = serde_json::from_value(schema).unwrap();
260/// ```
261///
262/// ## Complex Object Schema
263/// ```rust
264/// let schema = json!({
265///     "type": "object",
266///     "properties": {
267///         "users": {
268///             "type": "array",
269///             "items": {
270///                 "type": "object",
271///                 "properties": {
272///                     "id": { "type": "integer" },
273///                     "email": { "type": "string", "pattern": "^[^@]+@[^@]+$" }
274///                 },
275///                 "required": ["id", "email"]
276///             }
277///         }
278///     }
279/// });
280/// let parsed: Schema = serde_json::from_value(schema).unwrap();
281/// ```
282///
283/// ## Union Types with anyOf
284/// ```rust
285/// let schema = json!({
286///     "anyOf": [
287///         { "type": "string" },
288///         { "type": "integer", "minimum": 0 }
289///     ]
290/// });
291/// let parsed: Schema = serde_json::from_value(schema).unwrap();
292/// ```
293#[derive(Debug, Clone)]
294#[allow(dead_code)]
295pub struct Schema {
296    t: Rc<Type>,
297}
298
299#[allow(dead_code)]
300impl Schema {
301    fn new(t: Type) -> Self {
302        Schema { t: Rc::new(t) }
303    }
304
305    /// Returns a reference to the underlying type definition.
306    pub fn as_type(&self) -> &Type {
307        &self.t
308    }
309
310    /// Parse a JSON Schema document into a `Schema` instance.
311    /// Provides better error messages than `serde_json::from_value`.
312    pub fn from_serde_json_value(
313        schema: serde_json::Value,
314    ) -> Result<Self, Box<dyn core::error::Error + Send + Sync>> {
315        let meta_schema_validation_result = meta::validate_schema_detailed(&schema);
316        let schema = serde_json::from_value::<Schema>(schema)
317            .map_err(|e| format!("Failed to parse schema: {e}"))?;
318        if let Err(errors) = meta_schema_validation_result {
319            return Err(format!("Schema validation failed: {}", errors.join("\n")).into());
320        }
321
322        Ok(schema)
323    }
324
325    /// Parse a JSON Schema document from a string into a `Schema` instance.
326    /// Provides better error messages than `serde_json::from_str`.
327    pub fn from_json_str(s: &str) -> Result<Self, Box<dyn core::error::Error + Send + Sync>> {
328        let value: serde_json::Value =
329            serde_json::from_str(s).map_err(|e| format!("Failed to parse schema: {e}"))?;
330        Self::from_serde_json_value(value)
331    }
332
333    /// Validates a `Value` against this schema.
334    ///
335    /// Returns `Ok(())` if the value conforms to the schema, or a `ValidationError`
336    /// with detailed error information if validation fails.
337    ///
338    /// # Example
339    /// ```rust
340    /// use regorus::schema::Schema;
341    /// use regorus::Value;
342    /// use serde_json::json;
343    ///
344    /// let schema_json = json!({
345    ///     "type": "string",
346    ///     "minLength": 1,
347    ///     "maxLength": 10
348    /// });
349    /// let schema = Schema::from_serde_json_value(schema_json).unwrap();
350    /// let value = Value::from("hello");
351    ///
352    /// assert!(schema.validate(&value).is_ok());
353    /// ```
354    pub fn validate(&self, value: &Value) -> Result<(), error::ValidationError> {
355        validate::SchemaValidator::validate(value, self)
356    }
357}
358
359impl<'de> Deserialize<'de> for Schema {
360    /// Deserializes a JSON Schema into a `Schema` instance.
361    ///
362    /// This method handles the deserialization of JSON Schema documents into Regorus'
363    /// internal type system. It supports two main formats:
364    ///
365    /// 1. **Regular typed schemas** - Standard JSON Schema with a `type` field
366    /// 2. **Union schemas** - Schemas using `anyOf` to represent union types
367    ///
368    /// # Supported JSON Schema Formats
369    ///
370    /// ## Regular Type Schemas
371    ///
372    /// Standard JSON Schema documents with a `type` field are deserialized directly:
373    ///
374    /// ```json
375    /// {
376    ///   "type": "string",
377    ///   "minLength": 1,
378    ///   "maxLength": 100
379    /// }
380    /// ```
381    ///
382    /// ```json
383    /// {
384    ///   "type": "object",
385    ///   "properties": {
386    ///     "name": { "type": "string" },
387    ///     "age": { "type": "integer", "minimum": 0 }
388    ///   },
389    ///   "required": ["name"]
390    /// }
391    /// ```
392    ///
393    /// ## Union Type Schemas with anyOf
394    ///
395    /// Schemas using `anyOf` are converted to `Type::AnyOf` variants:
396    ///
397    /// ```json
398    /// {
399    ///   "anyOf": [
400    ///     { "type": "string" },
401    ///     { "type": "integer", "minimum": 0 },
402    ///     { "type": "null" }
403    ///   ]
404    /// }
405    /// ```
406    ///
407    /// # Error Handling
408    ///
409    /// This method will return a deserialization error if:
410    ///
411    /// - The JSON contains unknown/unsupported fields (due to `deny_unknown_fields`)
412    /// - The JSON structure doesn't match any supported schema format
413    /// - Individual type definitions within the schema are invalid
414    /// - Required fields are missing (e.g., `type` field for regular schemas)
415    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
416    where
417        D: Deserializer<'de>,
418    {
419        let v: serde_json::Value = Deserialize::deserialize(deserializer)?;
420        if v.get("anyOf").is_some() {
421            #[derive(Deserialize)]
422            #[serde(deny_unknown_fields)]
423            #[serde(rename_all = "camelCase")]
424            struct AnyOf {
425                #[serde(rename = "anyOf")]
426                variants: Rc<Vec<Schema>>,
427            }
428            let any_of: AnyOf = Deserialize::deserialize(v)
429                .map_err(|e| serde::de::Error::custom(format!("{e}")))?;
430            return Ok(Schema::new(Type::AnyOf(any_of.variants)));
431        }
432
433        if v.get("const").is_some() {
434            #[derive(Deserialize)]
435            #[serde(deny_unknown_fields)]
436            #[serde(rename_all = "camelCase")]
437            struct Const {
438                #[serde(rename = "const")]
439                value: Value,
440                description: Option<String>,
441            }
442            let const_schema: Const = Deserialize::deserialize(v)
443                .map_err(|e| serde::de::Error::custom(format!("{e}")))?;
444            return Ok(Schema::new(Type::Const {
445                description: const_schema.description,
446                value: const_schema.value,
447            }));
448        }
449        if v.get("enum").is_some() {
450            #[derive(Deserialize)]
451            #[serde(deny_unknown_fields)]
452            #[serde(rename_all = "camelCase")]
453            struct Enum {
454                #[serde(rename = "enum")]
455                values: Rc<Vec<Value>>,
456                description: Option<String>,
457            }
458            let enum_schema: Enum = Deserialize::deserialize(v)
459                .map_err(|e| serde::de::Error::custom(format!("{e}")))?;
460            return Ok(Schema::new(Type::Enum {
461                description: enum_schema.description,
462                values: enum_schema.values,
463            }));
464        }
465
466        let t: Type =
467            Deserialize::deserialize(v).map_err(|e| serde::de::Error::custom(format!("{e}")))?;
468        Ok(Schema::new(t))
469    }
470}
471
472#[derive(Debug, Clone, Deserialize)]
473// Use `type` when deserializing to discriminate between different types.
474#[serde(tag = "type")]
475// match JSON Schema casing.
476#[serde(rename_all = "camelCase")]
477// Raise error if unsupported fields are encountered.
478#[serde(deny_unknown_fields)]
479#[allow(dead_code)]
480pub enum Type {
481    /// Represents a type that can accept any JSON value.
482    ///
483    /// # Example
484    /// ```json
485    /// {
486    ///   "type": "any",
487    ///   "description": "Accepts any JSON value",
488    ///   "default": "fallback_value"
489    /// }
490    /// ```
491    Any {
492        description: Option<String>,
493        default: Option<Value>,
494    },
495
496    /// Represents a 64-bit signed integer type with optional range constraints.
497    ///
498    /// # Example
499    /// ```json
500    /// {
501    ///   "type": "integer",
502    ///   "description": "A whole number",
503    ///   "minimum": 0,
504    ///   "maximum": 100,
505    ///   "default": 50
506    /// }
507    /// ```
508    Integer {
509        description: Option<String>,
510        // In Bicep's type system, this is called minValue.
511        minimum: Option<i64>,
512        // In Bicep's type system, this is called maxValue.
513        maximum: Option<i64>,
514        default: Option<Value>,
515    },
516
517    /// Represents a 64-bit floating-point number type with optional range constraints.
518    ///
519    /// # Example
520    /// ```json
521    /// {
522    ///   "type": "number",
523    ///   "description": "A numeric value",
524    ///   "minimum": 0.0,
525    ///   "maximum": 1.0,
526    ///   "default": 0.5
527    /// }
528    /// ```
529    Number {
530        description: Option<String>,
531        minimum: Option<f64>,
532        maximum: Option<f64>,
533        default: Option<Value>,
534    },
535
536    /// Represents a boolean type that accepts `true` or `false` values.
537    ///
538    /// # Example
539    /// ```json
540    /// {
541    ///   "type": "boolean",
542    ///   "description": "A true/false value",
543    ///   "default": false
544    /// }
545    /// ```
546    Boolean {
547        description: Option<String>,
548        default: Option<Value>,
549    },
550
551    /// Represents the null type that only accepts JSON `null` values.
552    ///
553    /// # Example
554    /// ```json
555    /// {
556    ///   "type": "null",
557    ///   "description": "A null value"
558    /// }
559    /// ```
560    Null { description: Option<String> },
561
562    /// Represents a string type with optional length and pattern constraints.
563    ///
564    /// # Example
565    /// ```json
566    /// {
567    ///   "type": "string",
568    ///   "description": "Email address",
569    ///   "minLength": 1,
570    ///   "maxLength": 100,
571    ///   "pattern": "^[^@]+@[^@]+\\.[^@]+$",
572    /// }
573    /// ```
574    #[serde(rename_all = "camelCase")]
575    String {
576        description: Option<String>,
577        min_length: Option<usize>,
578        max_length: Option<usize>,
579        pattern: Option<String>,
580        default: Option<Value>,
581    },
582
583    /// Represents an array type with a specified item type and optional size constraints.
584    ///
585    /// # Example
586    /// ```json
587    /// {
588    ///   "type": "array",
589    ///   "description": "A list of items",
590    ///   "items": { "type": "string" },
591    ///   "minItems": 1,
592    ///   "maxItems": 10,
593    ///   "default": ["item1", "item2"]
594    /// }
595    /// ```
596    #[serde(rename_all = "camelCase")]
597    Array {
598        description: Option<String>,
599        items: Schema,
600        // In Bicep's type system, this is called minLength.
601        min_items: Option<usize>,
602        // In Bicep's type system, this is called maxLength.
603        max_items: Option<usize>,
604        default: Option<Value>,
605    },
606
607    /// Represents an object type with defined properties and optional constraints.
608    ///
609    /// The `Object` type accepts JSON objects with specified properties, required fields,
610    /// and optional additional properties schema. It can also support discriminated
611    /// unions through the `allOf` mechanism.
612    ///
613    /// # Examples
614    ///
615    /// ## Basic Object
616    /// ```json
617    /// {
618    ///   "type": "object",
619    ///   "properties": {
620    ///     "name": { "type": "string" },
621    ///     "age": { "type": "integer" }
622    ///   },
623    ///   "required": ["name"]
624    /// }
625    /// ```
626    ///
627    /// ## Object with Additional Properties
628    /// ```json
629    /// {
630    ///   "type": "object",
631    ///   "properties": {
632    ///     "core_field": { "type": "string" }
633    ///   },
634    ///   "additionalProperties": { "type": "any" },
635    ///   "description": "Extensible configuration object"
636    /// }
637    /// ```
638    ///
639    /// ## Discriminated Subobjects (Polymorphic Types)
640    ///
641    /// Discriminated subobjects allow modeling polymorphic types where the structure
642    /// depends on a discriminator field value. This is useful for representing different
643    /// types of resources, messages, or configurations that share common base properties
644    /// but have type-specific additional properties.
645    ///
646    /// ```json
647    /// {
648    ///   "type": "object",
649    ///   "description": "Azure resource definition",
650    ///   "properties": {
651    ///     "name": {
652    ///       "type": "string",
653    ///       "description": "Resource name"
654    ///     },
655    ///     "location": {
656    ///       "type": "string",
657    ///       "description": "Azure region"
658    ///     },
659    ///     "type": {
660    ///       "type": "string",
661    ///       "description": "Resource type discriminator"
662    ///     }
663    ///   },
664    ///   "required": ["name", "location", "type"],
665    ///   "allOf": [
666    ///     {
667    ///       "if": {
668    ///         "properties": {
669    ///           "type": { "const": "Microsoft.Compute/virtualMachines" }
670    ///         }
671    ///       },
672    ///       "then": {
673    ///         "properties": {
674    ///           "vmSize": {
675    ///             "type": "string",
676    ///             "description": "Virtual machine size"
677    ///           },
678    ///           "osType": {
679    ///             "type": "enum",
680    ///             "values": ["Windows", "Linux"]
681    ///           },
682    ///           "imageReference": {
683    ///             "type": "object",
684    ///             "properties": {
685    ///               "publisher": { "type": "string" },
686    ///               "offer": { "type": "string" },
687    ///               "sku": { "type": "string" }
688    ///             },
689    ///             "required": ["publisher", "offer", "sku"]
690    ///           }
691    ///         },
692    ///         "required": ["vmSize", "osType", "imageReference"]
693    ///       }
694    ///     },
695    ///     {
696    ///       "if": {
697    ///         "properties": {
698    ///           "type": { "const": "Microsoft.Storage/storageAccounts" }
699    ///         }
700    ///       },
701    ///       "then": {
702    ///         "properties": {
703    ///           "accountType": {
704    ///             "type": "enum",
705    ///             "values": ["Standard_LRS", "Standard_GRS", "Premium_LRS"]
706    ///           },
707    ///           "encryption": {
708    ///             "type": "object",
709    ///             "properties": {
710    ///               "services": {
711    ///                 "type": "object",
712    ///                 "properties": {
713    ///                   "blob": { "type": "boolean" },
714    ///                   "file": { "type": "boolean" }
715    ///                 }
716    ///               }
717    ///             }
718    ///           }
719    ///         },
720    ///         "required": ["accountType"]
721    ///       }
722    ///     },
723    ///     {
724    ///       "if": {
725    ///         "properties": {
726    ///           "type": { "const": "Microsoft.Network/virtualNetworks" }
727    ///         }
728    ///       },
729    ///       "then": {
730    ///         "properties": {
731    ///           "addressSpace": {
732    ///             "type": "object",
733    ///             "properties": {
734    ///               "addressPrefixes": {
735    ///                 "type": "array",
736    ///                 "items": { "type": "string" },
737    ///                 "minItems": 1
738    ///               }
739    ///             },
740    ///             "required": ["addressPrefixes"]
741    ///           },
742    ///           "subnets": {
743    ///             "type": "array",
744    ///             "items": {
745    ///               "type": "object",
746    ///               "properties": {
747    ///                 "name": { "type": "string" },
748    ///                 "addressPrefix": { "type": "string" }
749    ///               },
750    ///               "required": ["name", "addressPrefix"]
751    ///             }
752    ///           }
753    ///         },
754    ///         "required": ["addressSpace"]
755    ///       }
756    ///     }
757    ///   ]
758    /// }
759    /// ```
760    ///
761    /// ## Discriminated Subobject Structure
762    ///
763    /// When using discriminated subobjects:
764    ///
765    /// 1. **Base Properties**: Common properties shared by all variants (defined in the main `properties`)
766    /// 2. **Discriminator Field**: A property that determines which variant applies (e.g., `"kind"` field)
767    /// 3. **Variant-Specific Properties**: Additional properties that only apply to specific discriminator values
768    /// 4. **Conditional Schema**: Each `allOf` entry uses `if`/`then` to conditionally apply variant-specific schemas
769    ///
770    /// ## Message Type Example
771    ///
772    /// ```json
773    /// {
774    ///   "type": "object",
775    ///   "description": "Polymorphic message types",
776    ///   "properties": {
777    ///     "id": { "type": "string" },
778    ///     "timestamp": { "type": "integer" },
779    ///     "messageType": { "type": "string" }
780    ///   },
781    ///   "required": ["id", "timestamp", "messageType"],
782    ///   "allOf": [
783    ///     {
784    ///       "if": {
785    ///         "properties": {
786    ///           "messageType": { "const": "text" }
787    ///         }
788    ///       },
789    ///       "then": {
790    ///         "properties": {
791    ///           "content": { "type": "string", "minLength": 1 },
792    ///           "formatting": {
793    ///             "type": "enum",
794    ///             "values": ["plain", "markdown", "html"]
795    ///           }
796    ///         },
797    ///         "required": ["content"]
798    ///       }
799    ///     },
800    ///     {
801    ///       "if": {
802    ///         "properties": {
803    ///           "messageType": { "const": "image" }
804    ///         }
805    ///       },
806    ///       "then": {
807    ///         "properties": {
808    ///           "imageUrl": { "type": "string" },
809    ///           "altText": { "type": "string" },
810    ///           "width": { "type": "integer", "minimum": 1 },
811    ///           "height": { "type": "integer", "minimum": 1 }
812    ///         },
813    ///         "required": ["imageUrl"]
814    ///       }
815    ///     },
816    ///     {
817    ///       "if": {
818    ///         "properties": {
819    ///           "messageType": { "const": "file" }
820    ///         }
821    ///       },
822    ///       "then": {
823    ///         "properties": {
824    ///           "filename": { "type": "string" },
825    ///           "fileSize": { "type": "integer", "minimum": 0 },
826    ///           "mimeType": { "type": "string" },
827    ///           "downloadUrl": { "type": "string" }
828    ///         },
829    ///         "required": ["filename", "fileSize", "downloadUrl"]
830    ///       }
831    ///     }
832    ///   ]
833    /// }
834    /// ```
835    ///
836    /// This structure ensures that:
837    /// - All messages have `id`, `timestamp`, and `messageType` fields
838    /// - Text messages additionally require `content` and may have `formatting`
839    /// - Image messages require `imageUrl` and may specify dimensions
840    #[serde(rename_all = "camelCase")]
841    Object {
842        description: Option<String>,
843        #[serde(default)]
844        properties: Rc<BTreeMap<String, Schema>>,
845        #[serde(default)]
846        required: Option<Rc<Vec<String>>>,
847        #[serde(default = "additional_properties_default")]
848        #[serde(deserialize_with = "additional_properties_deserialize")]
849        additional_properties: Option<Schema>,
850
851        // This is a required property in Bicep's type system. There is not direct equivalent in JSON Schema.
852        // However, JSON Schema simply allows any schema to have a `name` property.
853        name: Option<String>,
854
855        default: Option<Value>,
856
857        #[serde(rename = "allOf")]
858        discriminated_subobject: Option<Rc<DiscriminatedSubobject>>,
859        // Bicep property attributes like `readOnly`, `writeOnly` are not needed in Regorus' type system.
860    },
861
862    /// Represents a union type that accepts values matching any of the specified schemas.
863    ///
864    /// The `AnyOf` type creates a union where a value is valid if it matches at least
865    /// one of the provided schemas. This is useful for optional types, alternative
866    /// formats, or polymorphic data structures.
867    ///
868    /// # JSON Schema Format
869    ///
870    /// ```json
871    /// {
872    ///   "anyOf": [
873    ///     { "type": "string" },
874    ///     { "type": "integer", "minimum": 0 },
875    ///     { "type": "null" }
876    ///   ]
877    /// }
878    /// ```
879    ///
880    /// AnyOf deserialization is handled by the `Schema` deserializer.
881    /// This is because, unlike other variant which can be distingished by the `type` field,
882    /// `anyOf` does not have a `type` field. Instead, it is a top-level field that contains an array of schemas.
883    #[serde(skip)]
884    AnyOf(Rc<Vec<Schema>>),
885
886    /// Represents a constant type that accepts only a single specific value.
887    ///
888    /// The `Const` type accepts only the exact value specified. This is useful for
889    /// literal values, discriminator fields, or when only one specific value is valid.
890    ///
891    /// # Example
892    ///
893    /// ```json
894    /// {
895    ///   "type": "const",
896    ///   "description": "A single constant value",
897    ///   "value": "specific_value"
898    /// }
899    /// ```
900    #[serde(skip)]
901    Const {
902        description: Option<String>,
903        value: Value,
904    },
905
906    /// Represents an enumeration type with a fixed set of allowed values.
907    ///
908    /// The `Enum` type accepts only values that are explicitly listed in the values array.
909    /// Values can be of any JSON type (strings, numbers, booleans, objects, arrays, null).
910    ///
911    /// # Example
912    /// ```json
913    /// {
914    ///   "type": "enum",
915    ///   "description": "A predefined set of values",
916    ///   "values": ["value1", "value2", 42, true, null]
917    /// }
918    /// ```
919    #[serde(skip)]
920    Enum {
921        description: Option<String>,
922        values: Rc<Vec<Value>>,
923    },
924
925    /// Specific to Rego. Needed for representing type of expressions involving sets.
926    #[serde(skip)]
927    Set {
928        description: Option<String>,
929        items: Schema,
930        default: Option<Value>,
931    },
932}
933
934// By default any additional properties are allowed.
935fn additional_properties_default() -> Option<Schema> {
936    // Default is to allow additional properties of any type.
937    Some(Schema::new(Type::Any {
938        description: None,
939        default: None,
940    }))
941}
942
943fn additional_properties_deserialize<'de, D>(deserializer: D) -> Result<Option<Schema>, D::Error>
944where
945    D: Deserializer<'de>,
946{
947    let value = serde_json::Value::deserialize(deserializer)?;
948    if let Some(b) = value.as_bool() {
949        if b {
950            // If additionalProperties is true, it means any type is allowed.
951            return Ok(Some(Schema::new(Type::Any {
952                description: None,
953                default: None,
954            })));
955        } else {
956            // If additionalProperties is false, no additional properties are allowed.
957            return Ok(None);
958        }
959    }
960
961    let schema: Schema = Deserialize::deserialize(value.clone())
962        .map_err(|e| serde::de::Error::custom(format!("{e}")))?;
963    Ok(Some(schema))
964}
965
966// A subobject is just like an object, but it cannot have a `discriminated_subobject` property.
967#[derive(Debug, Clone, Deserialize)]
968#[allow(dead_code)]
969pub struct Subobject {
970    pub description: Option<String>,
971    pub properties: Rc<BTreeMap<String, Schema>>,
972    pub required: Option<Rc<Vec<String>>>,
973    #[serde(default = "additional_properties_default")]
974    #[serde(deserialize_with = "additional_properties_deserialize")]
975    pub additional_properties: Option<Schema>,
976    // This is a required property in Bicep's type system. There is not direct equivalent in JSON Schema.
977    // However, JSON Schema simply allows any schema to have a `name` property.
978    pub name: Option<String>,
979}
980
981#[derive(Debug, Clone)]
982#[allow(dead_code)]
983pub struct DiscriminatedSubobject {
984    pub discriminator: String,
985    pub variants: Rc<BTreeMap<String, Subobject>>,
986}
987
988mod discriminated_subobject {
989    use super::*;
990
991    #[derive(Debug, Deserialize)]
992    #[serde(deny_unknown_fields)]
993    pub struct DiscriminatorValue {
994        #[serde(rename = "const")]
995        pub value: String,
996    }
997
998    #[derive(Debug, Deserialize)]
999    #[serde(deny_unknown_fields)]
1000    pub struct DiscriminatorValueSpecification {
1001        pub properties: BTreeMap<String, DiscriminatorValue>,
1002    }
1003
1004    #[derive(Debug, Deserialize)]
1005    #[serde(deny_unknown_fields)]
1006    pub struct IfThen {
1007        #[serde(rename = "if")]
1008        pub discriminator_spec: DiscriminatorValueSpecification,
1009        #[serde(rename = "then")]
1010        pub subobject: Subobject,
1011    }
1012}
1013
1014impl<'de> Deserialize<'de> for DiscriminatedSubobject {
1015    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1016    where
1017        D: Deserializer<'de>,
1018    {
1019        let ifthens: Vec<discriminated_subobject::IfThen> = Deserialize::deserialize(deserializer)?;
1020        if ifthens.is_empty() {
1021            return Err(serde::de::Error::custom(
1022                "DiscriminatedSubobject must have at least one variant",
1023            ));
1024        }
1025        let mut discriminator = None;
1026        let mut variants = BTreeMap::new();
1027        for variant in ifthens.into_iter() {
1028            if variant.discriminator_spec.properties.len() != 1 {
1029                return Err(serde::de::Error::custom(
1030                    "DiscriminatedSubobject discriminator must have exactly one property",
1031                ));
1032            }
1033            if let Some((d, v)) = variant.discriminator_spec.properties.into_iter().next() {
1034                if let Some(discriminator) = &discriminator {
1035                    if d != *discriminator {
1036                        return Err(serde::de::Error::custom(
1037                            "DiscriminatedSubobject must have a single discriminator property",
1038                        ));
1039                    }
1040                } else {
1041                    discriminator = Some(d.clone());
1042                }
1043                variants.insert(v.value, variant.subobject);
1044            } else {
1045                return Err(serde::de::Error::custom(
1046                    "DiscriminatedSubobject discriminator must have exactly one property",
1047                ));
1048            }
1049        }
1050
1051        Ok(DiscriminatedSubobject {
1052            discriminator: discriminator.ok_or_else(|| {
1053                serde::de::Error::custom(
1054                    "DiscriminatedSubobject must have a discriminator property",
1055                )
1056            })?,
1057            variants: Rc::new(variants),
1058        })
1059    }
1060}
1061
1062#[cfg(test)]
1063mod tests {
1064    mod azure;
1065    mod suite;
1066    mod validate {
1067        mod effect;
1068        mod resource;
1069    }
1070}