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}