From 2b20e8dae8aa98442748984cf1edbd45e1c0c897 Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Wed, 11 Jun 2025 18:21:54 +0200 Subject: [PATCH] Reflect info and stuff --- crates/bevy_remote/src/schemas/json_schema.rs | 207 ++--- crates/bevy_remote/src/schemas/mod.rs | 10 +- .../bevy_remote/src/schemas/reflect_info.rs | 757 +++++++++++------- 3 files changed, 557 insertions(+), 417 deletions(-) diff --git a/crates/bevy_remote/src/schemas/json_schema.rs b/crates/bevy_remote/src/schemas/json_schema.rs index 1bd1a78e97..ffb5d1a10a 100644 --- a/crates/bevy_remote/src/schemas/json_schema.rs +++ b/crates/bevy_remote/src/schemas/json_schema.rs @@ -2,12 +2,10 @@ //! It tries to follow this standard: use alloc::borrow::Cow; use bevy_platform::collections::HashMap; -use bevy_reflect::{ - GetTypeRegistration, NamedField, OpaqueInfo, Reflect, TypeInfo, TypeRegistration, TypeRegistry, -}; +use bevy_reflect::{GetTypeRegistration, Reflect, TypeRegistration, TypeRegistry}; use core::any::TypeId; use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; +use serde_json::Value; use crate::schemas::{ reflect_info::{SchemaInfoReflect, SchemaNumber}, @@ -60,72 +58,7 @@ impl From<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType { return JsonSchemaBevyType::default(); }; typed_schema.reflect_types = metadata.get_registered_reflect_types(reg); - match type_info { - TypeInfo::Struct(info) => { - // typed_schema.properties = info - // .iter() - // .map(|field| (field.name().to_owned(), field.build_schema())) - // .collect::>(); - // typed_schema.required = info - // .iter() - // .filter(|field| !field.type_path().starts_with("core::option::Option")) - // .map(|f| f.name().to_owned()) - // .collect::>(); - // typed_schema.additional_properties = Some(false); - // typed_schema.schema_type = Some(SchemaType::Object); - // typed_schema.kind = SchemaKind::Struct; - } - TypeInfo::Enum(info) => { - typed_schema.kind = SchemaKind::Enum; - typed_schema.one_of = info - .iter() - .map(|variant| variant.build_schema()) - .collect::>(); - } - TypeInfo::TupleStruct(info) => { - typed_schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Array)); - typed_schema.kind = SchemaKind::TupleStruct; - // typed_schema.prefix_items = info - // .iter() - // .map(SchemaJsonReference::ref_type) - // .collect::>(); - // typed_schema.items = Some(false.into()); - } - TypeInfo::List(info) => { - typed_schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Array)); - typed_schema.kind = SchemaKind::List; - // typed_schema.items = info.item_ty().ref_type().into(); - } - TypeInfo::Array(info) => { - typed_schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Array)); - typed_schema.kind = SchemaKind::Array; - // typed_schema.items = info.item_ty().ref_type().into(); - } - TypeInfo::Map(info) => { - typed_schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Array)); - typed_schema.kind = SchemaKind::Map; - typed_schema.key_type = info.key_ty().ref_type().into(); - typed_schema.value_type = info.value_ty().ref_type().into(); - } - TypeInfo::Tuple(info) => { - typed_schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Array)); - typed_schema.kind = SchemaKind::Tuple; - // typed_schema.prefix_items = info - // .iter() - // .map(SchemaJsonReference::ref_type) - // .collect::>(); - // typed_schema.items = Some(false.into()); - } - TypeInfo::Set(info) => { - typed_schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Array)); - typed_schema.kind = SchemaKind::Set; - // typed_schema.items = info.value_ty().ref_type().into(); - } - TypeInfo::Opaque(info) => { - typed_schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Array)); - typed_schema.kind = SchemaKind::Value; - } - }; + *typed_schema } } @@ -138,8 +71,10 @@ impl From<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType { #[serde(rename_all = "camelCase")] pub struct JsonSchemaBevyType { /// Bevy specific field, short path of the type. + #[serde(skip_serializing_if = "String::is_empty", default)] pub short_path: String, /// Bevy specific field, full path of the type. + #[serde(skip_serializing_if = "String::is_empty", default)] pub type_path: String, /// Bevy specific field, path of the module that type is part of. #[serde(skip_serializing_if = "Option::is_none", default)] @@ -156,12 +91,12 @@ pub struct JsonSchemaBevyType { /// /// It contains type info of key of the Map. #[serde(skip_serializing_if = "Option::is_none", default)] - pub key_type: Option, + pub key_type: Option, /// Bevy specific field, provided when [`SchemaKind`] `kind` field is equal to [`SchemaKind::Map`]. /// /// It contains type info of value of the Map. #[serde(skip_serializing_if = "Option::is_none", default)] - pub value_type: Option, + pub value_type: Option, /// The type keyword is fundamental to JSON Schema. It specifies the data type for a schema. #[serde(skip_serializing_if = "Option::is_none", default)] #[serde(rename = "type")] @@ -171,7 +106,7 @@ pub struct JsonSchemaBevyType { /// Validation with "additionalProperties" applies only to the child /// values of instance names that do not appear in the annotation results of either "properties" or "patternProperties". #[serde(skip_serializing_if = "Option::is_none", default)] - pub additional_properties: Option, + pub additional_properties: Option, /// Validation succeeds if, for each name that appears in both the instance and as a name /// within this keyword's value, the child instance for that name successfully validates /// against the corresponding schema. @@ -202,12 +137,39 @@ pub struct JsonSchemaBevyType { /// array elements have been evaluated against this keyword's subschema. #[serde(skip_serializing_if = "Option::is_none", default)] pub items: Option, + /// The value of this keyword MUST be a non-negative integer. + /// An array instance is valid against "minItems" if its size is greater than, + /// or equal to, the value of this keyword. + /// Omitting this keyword has the same behavior as a value of 0. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub min_items: Option, + /// The value of this keyword MUST be a non-negative integer. + /// An array instance is valid against "maxItems" if its size is less than, + /// or equal to, the value of this keyword. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub max_items: Option, + /// The value of "minimum" MUST be a number, + /// representing an inclusive lower limit for a numeric instance. + /// If the instance is a number, then this keyword validates only + /// if the instance is greater than or exactly equal to "minimum". #[serde(skip_serializing_if = "Option::is_none", default)] pub minimum: Option, + /// The value of "maximum" MUST be a number, + /// representing an inclusive upper limit for a numeric instance. + /// If the instance is a number, then this keyword validates only + /// if the instance is less than or exactly equal to "maximum". #[serde(skip_serializing_if = "Option::is_none", default)] pub maximum: Option, + /// The value of "exclusiveMinimum" MUST be a number, + /// representing an exclusive lower limit for a numeric instance. + /// If the instance is a number, then this keyword validates only + /// if the instance is greater than "exclusiveMinimum". #[serde(skip_serializing_if = "Option::is_none", default)] pub exclusive_minimum: Option, + /// The value of "exclusiveMaximum" MUST be a number, + /// representing an exclusive upper limit for a numeric instance. + /// If the instance is a number, then this keyword validates only + /// if the instance is less than "exclusiveMaximum". #[serde(skip_serializing_if = "Option::is_none", default)] pub exclusive_maximum: Option, /// Type description @@ -215,20 +177,52 @@ pub struct JsonSchemaBevyType { pub description: Option, } +/// Represents different types of JSON Schema values that can be used in schema definitions. +/// +/// This enum supports the various ways a JSON Schema property can be defined, +/// including boolean values, constant values, and complex schema objects. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Reflect)] #[serde(untagged)] pub enum JsonSchemaVariant { + /// A simple boolean value used in schema definitions. + /// + /// This is commonly used for properties like `additionalProperties` where + /// a boolean true/false indicates whether additional properties are allowed. BoolValue(bool), + /// A constant value with an optional description. + /// + /// This variant represents a JSON Schema `const` keyword, which specifies + /// that a value must be exactly equal to the given constant value. Const { + /// The constant value that must be matched exactly. #[reflect(ignore)] #[serde(rename = "const")] value: Value, + /// Optional human-readable description of the constant value. #[serde(skip_serializing_if = "Option::is_none", default)] description: Option, }, + /// A full JSON Schema definition. + /// + /// This variant contains a complete schema object that defines the structure, + /// validation rules, and metadata for a particular type or property. Schema(#[reflect(ignore)] Box), } + impl JsonSchemaVariant { + /// Creates a new constant value variant from any serializable type. + /// + /// This is a convenience constructor that serializes the provided value + /// to JSON and wraps it in the `Const` variant with an optional description. + /// + /// # Arguments + /// + /// * `serializable` - Any value that implements `Serialize` + /// * `description` - Optional description for the constant value + /// + /// # Returns + /// + /// A new `JsonSchemaVariant::Const` with the serialized value pub fn const_value(serializable: impl Serialize, description: Option) -> Self { Self::Const { value: serde_json::to_value(serializable).unwrap_or_default(), @@ -264,10 +258,20 @@ pub enum SchemaKind { /// Optional type Optional, } +/// Represents the possible type variants for a JSON Schema. +/// +/// In JSON Schema, the `type` keyword can either specify a single type +/// or an array of types to allow multiple valid types for a property. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Reflect, Eq, PartialOrd, Ord)] #[serde(untagged)] pub enum SchemaTypeVariant { + /// A single schema type (e.g., "string", "number", "object"). + /// This is the most common case where a property has exactly one valid type. Single(SchemaType), + /// Multiple schema types allowed for the same property. + /// This variant is used when a property can accept multiple types, + /// such as allowing both "string" and "number" for the same field. + /// In Rust case it most often means it is a Option type. Multiple(Vec), } @@ -346,58 +350,6 @@ impl SchemaType { } } -/// Helper trait for generating json schema reference -trait SchemaJsonReference { - /// Reference to another type in schema. - /// The value `$ref` is a URI-reference that is resolved against the schema. - fn ref_type(self) -> Value; -} - -/// Helper trait for mapping bevy type path into json schema type -pub trait SchemaJsonType { - /// Bevy Reflect type path - fn get_type_path(&self) -> &'static str; - - /// JSON Schema type keyword from Bevy reflect type path into - fn map_json_type(&self) -> SchemaType { - match self.get_type_path() { - "bool" => SchemaType::Boolean, - "u8" | "u16" | "u32" | "u64" | "u128" | "usize" => SchemaType::Integer, - "i8" | "i16" | "i32" | "i64" | "i128" | "isize" => SchemaType::Integer, - "f32" | "f64" => SchemaType::Number, - "char" | "str" | "alloc::string::String" => SchemaType::String, - _ => SchemaType::Object, - } - } -} - -impl SchemaJsonType for OpaqueInfo { - fn get_type_path(&self) -> &'static str { - self.type_path() - } -} - -impl SchemaJsonReference for &bevy_reflect::Type { - fn ref_type(self) -> Value { - let path = self.path(); - json!({"type": json!({ "$ref": format!("#/$defs/{path}") })}) - } -} - -impl SchemaJsonReference for &bevy_reflect::UnnamedField { - fn ref_type(self) -> Value { - let path = self.type_path(); - json!({"type": json!({ "$ref": format!("#/$defs/{path}") })}) - } -} - -impl SchemaJsonReference for &NamedField { - fn ref_type(self) -> Value { - let type_path = self.type_path(); - json!({"type": json!({ "$ref": format!("#/$defs/{type_path}") }), "typePath": self.name()}) - } -} - #[cfg(test)] mod tests { use super::*; @@ -407,6 +359,7 @@ mod tests { use bevy_ecs::{component::Component, reflect::AppTypeRegistry, resource::Resource}; use bevy_reflect::prelude::ReflectDefault; use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; + use serde_json::json; #[test] fn reflect_export_struct() { @@ -654,8 +607,8 @@ mod tests { "minimum": 0, "type": "integer", "description": "Test doc", - "shortPath": "", - "typePath": "", + "shortPath": "u16", + "typePath": "u16", }, }, diff --git a/crates/bevy_remote/src/schemas/mod.rs b/crates/bevy_remote/src/schemas/mod.rs index 9766b1bea0..ad5af3047b 100644 --- a/crates/bevy_remote/src/schemas/mod.rs +++ b/crates/bevy_remote/src/schemas/mod.rs @@ -14,12 +14,12 @@ pub mod json_schema; pub mod open_rpc; pub mod reflect_info; -/// Holds mapping of reflect [type data](TypeData) to strings, +/// Holds mapping of reflect [type data](TypeData) to human-readable type names, /// later on used in Bevy Json Schema. #[derive(Debug, Resource, Reflect)] #[reflect(Resource)] pub struct SchemaTypesMetadata { - /// Type Data id mapping to strings. + /// Type Data id mapping to human-readable type names. pub type_data_map: HashMap, } @@ -42,7 +42,7 @@ impl Default for SchemaTypesMetadata { } impl SchemaTypesMetadata { - /// Map `TypeId` of `TypeData` to string + /// Map `TypeId` of `TypeData` to a human-readable type name pub fn map_type_data(&mut self, name: impl Into) { self.type_data_map.insert(TypeId::of::(), name.into()); } @@ -55,12 +55,12 @@ impl SchemaTypesMetadata { .collect() } - /// Checks if slice contains string value that matches checked `TypeData` + /// Checks if slice contains a type name that matches the checked `TypeData` pub fn has_type_data(&self, types_string_slice: &[String]) -> bool { self.has_type_data_by_id(TypeId::of::(), types_string_slice) } - /// Checks if slice contains string value that matches checked `TypeData` by id. + /// Checks if slice contains a type name that matches the checked `TypeData` by id. pub fn has_type_data_by_id(&self, id: TypeId, types_string_slice: &[String]) -> bool { self.type_data_map .get(&id) diff --git a/crates/bevy_remote/src/schemas/reflect_info.rs b/crates/bevy_remote/src/schemas/reflect_info.rs index 8db315cf71..9d8a66c248 100644 --- a/crates/bevy_remote/src/schemas/reflect_info.rs +++ b/crates/bevy_remote/src/schemas/reflect_info.rs @@ -1,12 +1,9 @@ //! Module containing information about reflected types. -use bevy_reflect::{ - GenericInfo, NamedField, Reflect, StructVariantInfo, TypeInfo, UnnamedField, VariantInfo, -}; +use bevy_reflect::{GenericInfo, NamedField, Reflect, TypeInfo, UnnamedField, VariantInfo}; use core::any::TypeId; use serde::{Deserialize, Serialize}; use std::{ any::Any, - f32::consts::PI, fmt::Debug, ops::{Bound, RangeBounds}, }; @@ -16,12 +13,12 @@ use crate::schemas::json_schema::{ }; /// Enum representing a number in schema. -#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Reflect)] +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Reflect, PartialOrd)] #[serde(untagged)] pub enum SchemaNumber { /// Integer value. Int(i128), - /// Always finite. + /// Floating-point value. Float(f64), } @@ -86,16 +83,74 @@ impl From for SchemaNumber { SchemaNumber::Int(value as i128) } } +/// Represents a bound value that can be either inclusive or exclusive. +/// Used to define range constraints for numeric types in JSON schema. +#[derive(Clone, Debug, Serialize, Deserialize, Copy, PartialEq, Reflect)] +pub enum BoundValue { + /// An inclusive bound that includes the specified value in the range. + Inclusive(SchemaNumber), + /// An exclusive bound that excludes the specified value from the range. + Exclusive(SchemaNumber), +} +impl BoundValue { + /// Returns the value if this is an inclusive bound, otherwise returns None. + fn get_inclusive(&self) -> Option { + match self { + BoundValue::Inclusive(v) => Some(*v), + _ => None, + } + } + /// Returns the value if this is an exclusive bound, otherwise returns None. + fn get_exclusive(&self) -> Option { + match self { + BoundValue::Exclusive(v) => Some(*v), + _ => None, + } + } +} + +/// Represents minimum and maximum value constraints for numeric types. +/// Used to define valid ranges for schema validation. #[derive(Clone, Debug, Default, Serialize, Deserialize, Copy, PartialEq, Reflect)] pub struct MinMaxValues { - pub min: Option, - pub min_exclusive: Option, - pub max: Option, - pub max_exclusive: Option, + /// The minimum bound value, if any. + pub min: Option, + /// The maximum bound value, if any. + pub max: Option, } impl MinMaxValues { + /// Checks if a given value falls within the defined range constraints. + /// Returns true if the value is within bounds, false otherwise. + pub fn in_range(&self, value: SchemaNumber) -> bool { + if let Some(min) = self.min { + if let Some(min_value) = min.get_inclusive() { + if value < min_value { + return false; + } + } else if let Some(min_value) = min.get_exclusive() { + if value <= min_value { + return false; + } + } + } + if let Some(max) = self.max { + if let Some(max_value) = max.get_inclusive() { + if value > max_value { + return false; + } + } else if let Some(max_value) = max.get_exclusive() { + if value >= max_value { + return false; + } + } + } + true + } + /// Creates MinMaxValues from a reflected range type. + /// Attempts to downcast the reflected value to the specified range type T + /// and extract its bounds. pub fn from_reflect(reflect_val: &dyn Reflect) -> Option where T: 'static + RangeBounds, @@ -110,299 +165,333 @@ impl MinMaxValues { ))) } + /// Creates MinMaxValues from range bounds and a type identifier. + /// Takes a tuple containing start bound, end bound, and TypeId to construct + /// the appropriate range constraints. pub fn from_range(value: (Bound<&T>, Bound<&T>, TypeId)) -> MinMaxValues where T: 'static + Into + Copy + Debug, { let base: MinMaxValues = value.2.into(); - let (min, min_exclusive) = match value.0 { - Bound::Included(v) => (Some((*v).into()), None), - Bound::Excluded(v) => (None, Some((*v).into())), - Bound::Unbounded => (base.min, None), + let min = match value.0 { + Bound::Included(v) => Some(BoundValue::Inclusive((*v).into())), + Bound::Excluded(v) => Some(BoundValue::Exclusive((*v).into())), + Bound::Unbounded => base.min, }; - let (max, max_exclusive) = match value.1 { - Bound::Included(v) => (Some((*v).into()), None), - Bound::Excluded(v) => (None, Some((*v).into())), - Bound::Unbounded => (base.max, None), + let max = match value.1 { + Bound::Included(v) => Some(BoundValue::Inclusive((*v).into())), + Bound::Excluded(v) => Some(BoundValue::Exclusive((*v).into())), + Bound::Unbounded => base.max, }; - Self { - min, - min_exclusive, - max, - max_exclusive, - } + Self { min, max } } } impl From for MinMaxValues { fn from(value: TypeId) -> Self { - let mut min: Option = None; - let mut max: Option = None; + let mut min: Option = None; + let mut max: Option = None; if value.eq(&TypeId::of::()) { - min = Some(0.into()); - max = Some(u8::MAX.into()); + min = Some(BoundValue::Inclusive(0.into())); + max = Some(BoundValue::Inclusive(u8::MAX.into())); } else if value.eq(&TypeId::of::()) { - min = Some(0.into()); - max = Some(u16::MAX.into()); + min = Some(BoundValue::Inclusive(0.into())); + max = Some(BoundValue::Inclusive(u16::MAX.into())); } else if value.eq(&TypeId::of::()) { - min = Some(0.into()); - max = Some(u32::MAX.into()); + min = Some(BoundValue::Inclusive(0.into())); + max = Some(BoundValue::Inclusive(u32::MAX.into())); } else if value.eq(&TypeId::of::()) { - min = Some(0.into()); + min = Some(BoundValue::Inclusive(0.into())); } else if value.eq(&TypeId::of::()) { - min = Some(0.into()); + min = Some(BoundValue::Inclusive(0.into())); } else if value.eq(&TypeId::of::()) { - min = Some(0.into()); + min = Some(BoundValue::Inclusive(0.into())); } else if value.eq(&TypeId::of::()) { - min = Some(i8::MIN.into()); - max = Some(i8::MAX.into()); + min = Some(BoundValue::Inclusive(i8::MIN.into())); + max = Some(BoundValue::Inclusive(i8::MAX.into())); } else if value.eq(&TypeId::of::()) { - min = Some(i16::MIN.into()); - max = Some(i16::MAX.into()); + min = Some(BoundValue::Inclusive(i16::MIN.into())); + max = Some(BoundValue::Inclusive(i16::MAX.into())); } else if value.eq(&TypeId::of::()) { - min = Some(i32::MIN.into()); - max = Some(i32::MAX.into()); - } - MinMaxValues { - min, - max, - min_exclusive: None, - max_exclusive: None, + min = Some(BoundValue::Inclusive(i32::MIN.into())); + max = Some(BoundValue::Inclusive(i32::MAX.into())); } + MinMaxValues { min, max } } } - +/// Enum representing the internal schema type information for different Rust types. +/// This enum categorizes how different types should be represented in JSON schema. #[derive(Clone, Debug, Default)] pub enum InternalSchemaType { - Primitive { - range: MinMaxValues, - schema_type: SchemaType, + /// Represents array-like types (Vec, arrays, lists, sets). + Array { + /// The TypeId of the element type contained in the array. + element_type: TypeId, + /// Optional type information for the element type. + element_type_info: Option, + /// Minimum number of elements allowed in the array. + min_size: Option, + /// Maximum number of elements allowed in the array. + max_size: Option, }, + /// Holds all variants of an enum type. + EnumHolder(Vec), + /// Represents a single enum variant. EnumVariant(VariantInfo), + /// Holds named fields for struct types. NamedFieldsHolder(Vec), + /// Holds unnamed fields for tuple and tuple struct types. UnnamedFieldsHolder(Vec), + /// Represents an Optional type (e.g., Option). Optional { + /// Generic information about the wrapped type T in Option. generic: GenericInfo, - range: MinMaxValues, - schema_type: SchemaType, }, + /// Represents a Map type (e.g., HashMap). + Map { + /// The TypeId of the key type contained in the map. + key_type: TypeId, + /// Optional type information for the key type. + key_type_info: Option, + /// The TypeId of the value type contained in the map. + value_type: TypeId, + /// Optional type information for the value type. + value_type_info: Option, + }, + /// Default variant for regular primitive types and other simple types. #[default] Regular, } +impl From<&SchemaTypeInfo> for Option { + fn from(value: &SchemaTypeInfo) -> Self { + match &value.internal_type { + InternalSchemaType::Map { .. } => Some(SchemaTypeVariant::Single(SchemaType::Object)), + InternalSchemaType::Array { .. } => Some(SchemaTypeVariant::Single(SchemaType::Array)), + InternalSchemaType::EnumHolder { .. } => None, + InternalSchemaType::EnumVariant(variant) => match variant { + VariantInfo::Unit(_) => Some(SchemaTypeVariant::Single(SchemaType::String)), + _ => Some(SchemaTypeVariant::Single(SchemaType::Object)), + }, + InternalSchemaType::NamedFieldsHolder { .. } => { + Some(SchemaTypeVariant::Single(SchemaType::Object)) + } + InternalSchemaType::UnnamedFieldsHolder(unnamed_fields) => { + if unnamed_fields.len() == 1 { + (&unnamed_fields[0].build_schema_type_info()).into() + } else { + Some(SchemaTypeVariant::Single(SchemaType::Array)) + } + } + InternalSchemaType::Optional { generic } => { + let schema_type = if let Some(SchemaTypeVariant::Single(gen_schema)) = + (&generic.ty().id().build_schema_type_info()).into() + { + gen_schema + } else { + SchemaType::Object + }; + Some(SchemaTypeVariant::Multiple(vec![ + schema_type, + SchemaType::Null, + ])) + } + InternalSchemaType::Regular => match &value.type_id { + Some(s) => Some(SchemaTypeVariant::Single(s.clone().into())), + _ => None, + }, + } + } +} + +/// Contains comprehensive information about a type's schema representation. +/// This struct aggregates all the necessary information to generate a JSON schema +/// from Rust type information obtained through reflection. #[derive(Clone, Debug, Default)] pub struct SchemaTypeInfo { + /// The internal categorization of the schema type. pub internal_type: InternalSchemaType, + /// Optional documentation string extracted from the type. pub documentation: Option, + /// The kind of schema (struct, enum, value, etc.). pub kind: SchemaKind, + /// Optional Bevy reflection type information. pub type_info: Option, + /// Optional TypeId for the type. + pub type_id: Option, + /// Numeric range constraints for the type. + pub range: MinMaxValues, } impl Into for SchemaTypeInfo { fn into(self) -> JsonSchemaVariant { + let schema_type: Option = (&self).into(); + let mut schema = JsonSchemaBevyType { + schema_type: schema_type.clone(), + kind: self.kind.clone(), + description: self.documentation.clone(), + type_path: self + .type_info + .as_ref() + .and_then(|s| Some(s.type_path_table().path().to_owned())) + .unwrap_or_default(), + short_path: self + .type_info + .as_ref() + .and_then(|s| Some(s.type_path_table().short_path().to_owned())) + .unwrap_or_default(), + crate_name: self + .type_info + .as_ref() + .and_then(|s| s.type_path_table().crate_name().map(str::to_owned)), + module_path: self + .type_info + .as_ref() + .and_then(|s| s.type_path_table().module_path().map(str::to_owned)), + minimum: self.range.min.as_ref().and_then(|r| r.get_inclusive()), + maximum: self.range.max.as_ref().and_then(|r| r.get_inclusive()), + exclusive_minimum: self.range.min.as_ref().and_then(|r| r.get_exclusive()), + exclusive_maximum: self.range.max.as_ref().and_then(|r| r.get_exclusive()), + ..Default::default() + }; + match self.internal_type { - InternalSchemaType::Primitive { range, schema_type } => { - JsonSchemaVariant::Schema(Box::new(JsonSchemaBevyType { - kind: self.kind, - schema_type: Some(SchemaTypeVariant::Single(schema_type)), - minimum: range.min, - maximum: range.max, - exclusive_minimum: range.min_exclusive, - exclusive_maximum: range.max_exclusive, - description: self.documentation, - ..Default::default() - })) + InternalSchemaType::Map { + key_type, + key_type_info, + value_type, + value_type_info, + } => { + schema.additional_properties = match &value_type_info { + Some(info) => Some(info.build_schema()), + None => Some(value_type.build_schema()), + }; + schema.value_type = match &value_type_info { + Some(info) => Some(info.build_schema()), + None => Some(value_type.build_schema()), + }; + schema.key_type = match &key_type_info { + Some(info) => Some(info.build_schema()), + None => Some(key_type.build_schema()), + }; } - InternalSchemaType::Regular => { - JsonSchemaVariant::Schema(Box::new(JsonSchemaBevyType { - type_path: self - .type_info - .as_ref() - .and_then(|s| Some(s.type_path_table().path().to_owned())) - .unwrap_or_default(), - short_path: self - .type_info - .as_ref() - .and_then(|s| Some(s.type_path_table().short_path().to_owned())) - .unwrap_or_default(), - crate_name: self - .type_info - .as_ref() - .and_then(|s| s.type_path_table().crate_name().map(str::to_owned)), - module_path: self - .type_info - .as_ref() - .and_then(|s| s.type_path_table().module_path().map(str::to_owned)), - description: self.documentation, - kind: self.kind, - ..Default::default() - })) + InternalSchemaType::Regular => {} + InternalSchemaType::EnumHolder(variants) => { + schema.one_of = variants.iter().map(|v| v.build_schema()).collect(); } InternalSchemaType::EnumVariant(variant_info) => match &variant_info { VariantInfo::Struct(struct_variant_info) => { + schema.kind = SchemaKind::Value; + schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Object)); let internal_type = InternalSchemaType::NamedFieldsHolder( struct_variant_info.iter().cloned().collect(), ); - let schema = SchemaTypeInfo { + let schema_field = SchemaTypeInfo { internal_type, documentation: variant_info.to_description(), kind: SchemaKind::Struct, type_info: None, + type_id: Some(struct_variant_info.type_id()), + range: MinMaxValues::default(), }; - JsonSchemaVariant::Schema(Box::new(JsonSchemaBevyType { - description: self.documentation, - kind: SchemaKind::Value, - schema_type: Some(SchemaTypeVariant::Single(SchemaType::Object)), - properties: [(variant_info.name().to_string(), schema.into())].into(), - required: vec![variant_info.name().to_string()], - ..Default::default() - })) + + schema.properties = + [(variant_info.name().to_string(), schema_field.into())].into(); + schema.required = vec![variant_info.name().to_string()]; } VariantInfo::Tuple(tuple_variant_info) => { + schema.kind = SchemaKind::Value; + schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Object)); let internal_type = InternalSchemaType::UnnamedFieldsHolder( tuple_variant_info.iter().cloned().collect(), ); - let schema = SchemaTypeInfo { + let schema_field = SchemaTypeInfo { internal_type, documentation: variant_info.to_description(), kind: SchemaKind::Tuple, type_info: None, + type_id: Some(tuple_variant_info.type_id()), + range: MinMaxValues::default(), }; - JsonSchemaVariant::Schema(Box::new(JsonSchemaBevyType { - description: self.documentation, - kind: SchemaKind::Value, - schema_type: Some(SchemaTypeVariant::Single(SchemaType::Object)), - properties: [(variant_info.name().to_string(), schema.into())].into(), - required: vec![variant_info.name().to_string()], - ..Default::default() - })) + schema.properties = + [(variant_info.name().to_string(), schema_field.into())].into(); + schema.required = vec![variant_info.name().to_string()]; + } + VariantInfo::Unit(unit_variant_info) => { + return JsonSchemaVariant::const_value( + unit_variant_info.name().to_string(), + schema.description.clone(), + ); } - VariantInfo::Unit(unit_variant_info) => JsonSchemaVariant::const_value( - unit_variant_info.name().to_string(), - self.documentation, - ), }, InternalSchemaType::NamedFieldsHolder(named_fields) => { - JsonSchemaVariant::Schema(Box::new(JsonSchemaBevyType { - kind: self.kind, - schema_type: Some(SchemaTypeVariant::Single(SchemaType::Object)), - description: self.documentation, - additional_properties: Some(false), - properties: named_fields - .iter() - .map(|field| (field.name().to_string(), field.build_schema())) - .collect(), - required: named_fields - .iter() - .map(|field| field.name().to_string()) - .collect(), - type_path: self - .type_info - .as_ref() - .and_then(|s| Some(s.type_path_table().path().to_owned())) - .unwrap_or_default(), - short_path: self - .type_info - .as_ref() - .and_then(|s| Some(s.type_path_table().short_path().to_owned())) - .unwrap_or_default(), - crate_name: self - .type_info - .as_ref() - .and_then(|s| s.type_path_table().crate_name().map(str::to_owned)), - module_path: self - .type_info - .as_ref() - .and_then(|s| s.type_path_table().module_path().map(str::to_owned)), - ..Default::default() - })) + schema.additional_properties = Some(JsonSchemaVariant::BoolValue(false)); + schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Object)); + schema.properties = named_fields + .iter() + .map(|field| (field.name().to_string(), field.build_schema())) + .collect(); + schema.required = named_fields + .iter() + .map(|field| field.name().to_string()) + .collect(); } InternalSchemaType::UnnamedFieldsHolder(unnamed_fields) => { if unnamed_fields.len() == 1 { - let s = unnamed_fields[0].build_schema(); - if let JsonSchemaVariant::Schema(mut schema) = s { - schema.kind = self.kind; - schema.description = self.documentation; - - schema.type_path = self - .type_info - .as_ref() - .and_then(|s| Some(s.type_path_table().path().to_owned())) - .unwrap_or_default(); - schema.short_path = self - .type_info - .as_ref() - .and_then(|s| Some(s.type_path_table().short_path().to_owned())) - .unwrap_or_default(); - schema.crate_name = self - .type_info - .as_ref() - .and_then(|s| s.type_path_table().crate_name().map(str::to_owned)); - schema.module_path = self - .type_info - .as_ref() - .and_then(|s| s.type_path_table().module_path().map(str::to_owned)); - JsonSchemaVariant::Schema(schema) + let new_schema = unnamed_fields[0].build_schema(); + if let JsonSchemaVariant::Schema(new_schema_type) = new_schema { + schema = *new_schema_type; + schema.schema_type = schema_type.clone(); + schema.description = self.documentation.clone(); } else { - s + return new_schema; } } else { - JsonSchemaVariant::Schema(Box::new(JsonSchemaBevyType { - description: self.documentation, - kind: self.kind, - additional_properties: Some(false), - prefix_items: unnamed_fields.iter().map(|s| s.build_schema()).collect(), - type_path: self - .type_info - .as_ref() - .and_then(|s| Some(s.type_path_table().path().to_owned())) - .unwrap_or_default(), - short_path: self - .type_info - .as_ref() - .and_then(|s| Some(s.type_path_table().short_path().to_owned())) - .unwrap_or_default(), - crate_name: self - .type_info - .as_ref() - .and_then(|s| s.type_path_table().crate_name().map(str::to_owned)), - module_path: self - .type_info - .as_ref() - .and_then(|s| s.type_path_table().module_path().map(str::to_owned)), - ..Default::default() - })) + schema.prefix_items = unnamed_fields.iter().map(|s| s.build_schema()).collect(); + schema.min_items = Some(unnamed_fields.len() as u64); + schema.max_items = Some(unnamed_fields.len() as u64); } } - InternalSchemaType::Optional { - generic, - range, - schema_type, + InternalSchemaType::Array { + element_type, + element_type_info, + min_size, + max_size, } => { + let items_schema = match element_type_info { + None => element_type.build_schema(), + Some(info) => info.build_schema(), + }; + schema.items = Some(items_schema); + schema.min_items = min_size; + schema.max_items = max_size; + } + InternalSchemaType::Optional { generic } => { let schema_variant = generic.ty().id().build_schema(); - if let JsonSchemaVariant::Schema(mut value) = schema_variant { - value.minimum = range.min; - value.maximum = range.max; - value.exclusive_minimum = range.min_exclusive; - value.exclusive_maximum = range.max_exclusive; - value.description = self.documentation; - value.kind = SchemaKind::Optional; - value.schema_type = Some(SchemaTypeVariant::Multiple(vec![ - schema_type, - SchemaType::Null, - ])); - JsonSchemaVariant::Schema(value) + if let JsonSchemaVariant::Schema(value) = schema_variant { + schema = *value; + schema.schema_type = schema_type.clone(); + schema.minimum = self.range.min.as_ref().and_then(|r| r.get_inclusive()); + schema.maximum = self.range.max.as_ref().and_then(|r| r.get_inclusive()); + schema.exclusive_minimum = + self.range.min.as_ref().and_then(|r| r.get_exclusive()); + schema.exclusive_maximum = + self.range.max.as_ref().and_then(|r| r.get_exclusive()); + schema.description = self.documentation; + schema.kind = SchemaKind::Optional; } else { - schema_variant + return schema_variant; } } } + JsonSchemaVariant::Schema(Box::new(schema)) } } /// Trait that builds the type information based on the reflected data. pub trait SchemaInfoReflect { + /// Returns the optional type information of the schema. fn try_get_optional_info(&self) -> Option { let type_info = self.try_get_type_info()?; let TypeInfo::Enum(enum_info) = type_info else { @@ -422,6 +511,28 @@ pub trait SchemaInfoReflect { fn try_get_type_info(&self) -> Option; /// Returns the Bevy kind of the schema. fn get_kind(&self) -> SchemaKind { + if self.try_get_optional_info().is_some() { + return SchemaKind::Optional; + }; + if SchemaType::try_get_primitive_type_from_type_id(self.get_type()).is_some() { + return SchemaKind::Value; + } + match self.try_get_type_info() { + Some(type_info) => { + return match type_info { + TypeInfo::Struct(_) => SchemaKind::Struct, + TypeInfo::TupleStruct(_) => SchemaKind::TupleStruct, + TypeInfo::Tuple(_) => SchemaKind::Tuple, + TypeInfo::List(_) => SchemaKind::List, + TypeInfo::Array(_) => SchemaKind::Array, + TypeInfo::Map(_) => SchemaKind::Map, + TypeInfo::Set(_) => SchemaKind::Set, + TypeInfo::Enum(_) => SchemaKind::Enum, + TypeInfo::Opaque(_) => SchemaKind::Opaque, + } + } + None => {} + } SchemaKind::Value } /// Builds the type information based on the reflected data. @@ -436,18 +547,15 @@ pub trait SchemaInfoReflect { internal_type, documentation: self.to_description(), kind: self.get_kind(), + range: self.get_range_by_id(), + type_id: Some(self.get_type()), } } + /// Builds the internal type information based on the reflected data. fn build_internal_type(&self) -> InternalSchemaType { if let Some(generic) = self.try_get_optional_info() { - let range = self.get_range_by_id(); - let schema_type: SchemaType = generic.ty().id().into(); - return InternalSchemaType::Optional { - generic, - range, - schema_type, - }; + return InternalSchemaType::Optional { generic }; } if let Some(type_info) = self.try_get_type_info() { match type_info { @@ -466,27 +574,48 @@ pub trait SchemaInfoReflect { tuple_info.iter().cloned().collect(), ); } - // TypeInfo::Enum(enum_info) => {} + TypeInfo::Enum(enum_info) => { + return InternalSchemaType::EnumHolder(enum_info.iter().cloned().collect()); + } - // TypeInfo::List(list_info) => todo!(), - // TypeInfo::Array(array_info) => todo!(), - // TypeInfo::Map(map_info) => todo!(), - // TypeInfo::Set(set_info) => todo!(), + TypeInfo::List(list_info) => { + return InternalSchemaType::Array { + element_type: list_info.item_ty().id(), + element_type_info: list_info.item_info().cloned(), + min_size: None, + max_size: None, + } + } + TypeInfo::Set(set_info) => { + return InternalSchemaType::Array { + element_type: set_info.value_ty().id(), + element_type_info: None, + min_size: None, + max_size: None, + } + } + TypeInfo::Array(array_info) => { + return InternalSchemaType::Array { + element_type: array_info.item_ty().id(), + element_type_info: array_info.item_info().cloned(), + min_size: Some(array_info.capacity() as u64), + max_size: Some(array_info.capacity() as u64), + } + } + TypeInfo::Map(map_info) => { + return InternalSchemaType::Map { + key_type: map_info.key_ty().id(), + key_type_info: map_info.key_info().cloned(), + value_type: map_info.value_ty().id(), + value_type_info: map_info.value_info().cloned(), + }; + } // // TypeInfo::Opaque(opaque_info) => todo!(), _ => {} } } - - let primitive_type = SchemaType::try_get_primitive_type_from_type_id(self.get_type()); - if let Some(s) = primitive_type { - InternalSchemaType::Primitive { - schema_type: s, - range: self.get_range_by_id(), - } - } else { - InternalSchemaType::Regular - } + InternalSchemaType::Regular } /// Builds the description based on the reflected data. @@ -508,6 +637,9 @@ pub trait SchemaInfoReflect { None } + /// Creates MinMaxValues from a reflected range type. + /// Attempts to downcast the reflected value to the specified range type T + /// and extract its bounds. fn min_max_from_attribute(&self) -> Option where T: 'static + RangeBounds, @@ -517,6 +649,9 @@ pub trait SchemaInfoReflect { .and_then(|reflect_value| MinMaxValues::from_reflect::(reflect_value)) } + /// Creates MinMaxValues from a reflected range type. + /// Attempts to downcast the reflected value to the specified range type T + /// and extract its bounds. fn min_max_from_attribute_for_type(&self) -> Option where T: 'static + Into + Copy + Debug, @@ -548,6 +683,9 @@ pub trait SchemaInfoReflect { None } + /// Creates MinMaxValues from a reflected range type. + /// Attempts to downcast the reflected value to the specified range type T + /// and extract its bounds. fn get_range_by_id(&self) -> MinMaxValues { let t = match self.try_get_optional_info() { Some(info) => info.ty().id(), @@ -569,6 +707,8 @@ pub trait SchemaInfoReflect { self.min_max_from_attribute_for_type::() } else if t.eq(&TypeId::of::()) { self.min_max_from_attribute_for_type::() + } else if t.eq(&TypeId::of::()) { + self.min_max_from_attribute_for_type::() } else if t.eq(&TypeId::of::()) { self.min_max_from_attribute_for_type::() } else if t.eq(&TypeId::of::()) { @@ -648,19 +788,6 @@ impl SchemaInfoReflect for TypeInfo { fn try_get_type_info(&self) -> Option { Some(self.clone()) } - fn get_kind(&self) -> SchemaKind { - match self { - TypeInfo::Struct(_) => SchemaKind::Struct, - TypeInfo::TupleStruct(_) => SchemaKind::TupleStruct, - TypeInfo::Tuple(_) => SchemaKind::Tuple, - TypeInfo::List(_) => SchemaKind::List, - TypeInfo::Array(_) => SchemaKind::Array, - TypeInfo::Map(_) => SchemaKind::Map, - TypeInfo::Set(_) => SchemaKind::Set, - TypeInfo::Enum(_) => SchemaKind::Enum, - TypeInfo::Opaque(_) => SchemaKind::Opaque, - } - } #[cfg(feature = "documentation")] fn get_docs(&self) -> Option<&str> { self.docs() @@ -685,6 +812,7 @@ impl SchemaInfoReflect for TypeId { #[cfg(test)] mod tests { + use bevy_platform::collections::HashMap; use bevy_reflect::GetTypeRegistration; use super::*; @@ -692,12 +820,16 @@ mod tests { #[test] fn integer_test() { let type_info = TypeId::of::().build_schema_type_info(); - let InternalSchemaType::Primitive { range, schema_type } = type_info.internal_type else { - return; - }; - assert_eq!(range.min, Some(0.into())); - assert_eq!(range.max, Some(u16::MAX.into())); - assert_eq!(schema_type, SchemaType::Integer); + let schema_type: Option = (&type_info).into(); + assert_eq!(type_info.range.min, Some(BoundValue::Inclusive(0.into()))); + assert_eq!( + type_info.range.max, + Some(BoundValue::Inclusive(u16::MAX.into())) + ); + assert_eq!( + schema_type, + Some(SchemaTypeVariant::Single(SchemaType::Integer)) + ); } #[test] @@ -714,12 +846,13 @@ mod tests { .expect("Should not fail"); let field_info = struct_info.field("no_value").expect("Should not fail"); let type_info = field_info.build_schema_type_info(); - let InternalSchemaType::Primitive { range, schema_type } = type_info.internal_type else { - return; - }; - assert_eq!(range.min, Some(10.into())); - assert_eq!(range.max, Some(13.into())); - assert_eq!(schema_type, SchemaType::Integer); + let schema_type: Option = (&type_info).into(); + assert_eq!(type_info.range.min, Some(BoundValue::Inclusive(10.into()))); + assert_eq!(type_info.range.max, Some(BoundValue::Inclusive(13.into()))); + assert_eq!( + schema_type, + Some(SchemaTypeVariant::Single(SchemaType::Integer)) + ); assert_eq!( type_info.documentation, Some("Test documentation".to_string()) @@ -749,14 +882,17 @@ mod tests { .expect("Should not fail"); let field_info = struct_info.field("no_value").expect("Should not fail"); let type_info = field_info.build_schema_type_info(); - let InternalSchemaType::Primitive { range, schema_type } = type_info.internal_type else { - return; - }; - eprintln!("Range: {:#?}", range); - assert_eq!(range.min, Some(0.into())); - assert_eq!(range.max, None); - assert_eq!(range.max_exclusive, Some(13.into())); - assert_eq!(schema_type, SchemaType::Integer); + let schema_type: Option = (&type_info).into(); + assert!(!type_info.range.in_range((-1).into())); + assert!(type_info.range.in_range(0.into())); + assert!(type_info.range.in_range(12.into())); + assert!(!type_info.range.in_range(13.into())); + assert_eq!(type_info.range.min, Some(BoundValue::Inclusive(0.into()))); + assert_eq!(type_info.range.max, Some(BoundValue::Exclusive(13.into()))); + assert_eq!( + schema_type, + Some(SchemaTypeVariant::Single(SchemaType::Integer)) + ); assert_eq!( type_info.documentation, Some("Test documentation".to_string()) @@ -786,13 +922,13 @@ mod tests { .expect("Should not fail"); let field_info = struct_info.iter().next().expect("Should not fail"); let type_info = field_info.build_schema_type_info(); - let InternalSchemaType::Primitive { range, schema_type } = type_info.internal_type else { - return; - }; - assert_eq!(range.min, Some(0.into())); - assert_eq!(range.max, None); - assert_eq!(range.max_exclusive, Some(13.into())); - assert_eq!(schema_type, SchemaType::Integer); + let schema_type: Option = (&type_info).into(); + assert_eq!(type_info.range.min, Some(BoundValue::Inclusive(0.into()))); + assert_eq!(type_info.range.max, Some(BoundValue::Exclusive(13.into()))); + assert_eq!( + schema_type, + Some(SchemaTypeVariant::Single(SchemaType::Integer)) + ); assert_eq!( type_info.documentation, Some("Test documentation".to_string()) @@ -814,20 +950,71 @@ mod tests { Variant4(usize), } eprintln!( - "{:#?}", - EnumTest::get_type_registration().type_info().build_schema() + "{}", + serde_json::to_string_pretty( + &EnumTest::get_type_registration().type_info().build_schema() + ) + .expect("") ); - let enum_info = EnumTest::get_type_registration() - .type_info() - .as_enum() - .expect("Should not fail"); - for field in enum_info.iter() { - let type_info = field.build_schema(); - eprintln!( - "{}: {}", - field.name(), - serde_json::to_string_pretty(&type_info).unwrap() - ); + } + + #[test] + fn reflect_struct_with_array() { + #[derive(Reflect, Default, Deserialize, Serialize)] + pub struct ArrayComponent { + pub arry: [i32; 3], } + eprintln!( + "{}", + serde_json::to_string_pretty( + &ArrayComponent::get_type_registration() + .type_info() + .build_schema() + ) + .expect("") + ); + } + + #[test] + fn reflect_struct_with_hashmap() { + #[derive(Reflect, Default, Deserialize, Serialize)] + pub struct HashMapStruct { + pub map: HashMap>, + } + assert!(serde_json::from_str::( + "{\"map\": {\"0\": 1, \"1\": 41, \"2\": null}}" + ) + .is_ok()); + + eprintln!( + "{}", + serde_json::to_string_pretty( + &HashMapStruct::get_type_registration() + .type_info() + .build_schema() + ) + .expect("") + ); + } + + #[test] + fn reflect_nested_struct() { + #[derive(Reflect, Default, Deserialize, Serialize)] + pub struct OtherStruct { + pub field: String, + } + #[derive(Reflect, Default, Deserialize, Serialize)] + pub struct NestedStruct { + pub other: OtherStruct, + } + eprintln!( + "{}", + serde_json::to_string_pretty( + &NestedStruct::get_type_registration() + .type_info() + .build_schema() + ) + .expect("") + ); } }