Reflect info and stuff

This commit is contained in:
Piotr Siuszko 2025-06-11 18:21:54 +02:00
parent 648bd3d796
commit 2b20e8dae8
3 changed files with 557 additions and 417 deletions

View File

@ -2,12 +2,10 @@
//! It tries to follow this standard: <https://json-schema.org/specification> //! It tries to follow this standard: <https://json-schema.org/specification>
use alloc::borrow::Cow; use alloc::borrow::Cow;
use bevy_platform::collections::HashMap; use bevy_platform::collections::HashMap;
use bevy_reflect::{ use bevy_reflect::{GetTypeRegistration, Reflect, TypeRegistration, TypeRegistry};
GetTypeRegistration, NamedField, OpaqueInfo, Reflect, TypeInfo, TypeRegistration, TypeRegistry,
};
use core::any::TypeId; use core::any::TypeId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{json, Value}; use serde_json::Value;
use crate::schemas::{ use crate::schemas::{
reflect_info::{SchemaInfoReflect, SchemaNumber}, reflect_info::{SchemaInfoReflect, SchemaNumber},
@ -60,72 +58,7 @@ impl From<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType {
return JsonSchemaBevyType::default(); return JsonSchemaBevyType::default();
}; };
typed_schema.reflect_types = metadata.get_registered_reflect_types(reg); 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::<HashMap<_, _>>();
// typed_schema.required = info
// .iter()
// .filter(|field| !field.type_path().starts_with("core::option::Option"))
// .map(|f| f.name().to_owned())
// .collect::<Vec<_>>();
// 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::<Vec<_>>();
}
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::<Vec<_>>();
// 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::<Vec<_>>();
// 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 *typed_schema
} }
} }
@ -138,8 +71,10 @@ impl From<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct JsonSchemaBevyType { pub struct JsonSchemaBevyType {
/// Bevy specific field, short path of the type. /// Bevy specific field, short path of the type.
#[serde(skip_serializing_if = "String::is_empty", default)]
pub short_path: String, pub short_path: String,
/// Bevy specific field, full path of the type. /// Bevy specific field, full path of the type.
#[serde(skip_serializing_if = "String::is_empty", default)]
pub type_path: String, pub type_path: String,
/// Bevy specific field, path of the module that type is part of. /// Bevy specific field, path of the module that type is part of.
#[serde(skip_serializing_if = "Option::is_none", default)] #[serde(skip_serializing_if = "Option::is_none", default)]
@ -156,12 +91,12 @@ pub struct JsonSchemaBevyType {
/// ///
/// It contains type info of key of the Map. /// It contains type info of key of the Map.
#[serde(skip_serializing_if = "Option::is_none", default)] #[serde(skip_serializing_if = "Option::is_none", default)]
pub key_type: Option<Value>, pub key_type: Option<JsonSchemaVariant>,
/// Bevy specific field, provided when [`SchemaKind`] `kind` field is equal to [`SchemaKind::Map`]. /// Bevy specific field, provided when [`SchemaKind`] `kind` field is equal to [`SchemaKind::Map`].
/// ///
/// It contains type info of value of the Map. /// It contains type info of value of the Map.
#[serde(skip_serializing_if = "Option::is_none", default)] #[serde(skip_serializing_if = "Option::is_none", default)]
pub value_type: Option<Value>, pub value_type: Option<JsonSchemaVariant>,
/// The type keyword is fundamental to JSON Schema. It specifies the data type for a schema. /// 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(skip_serializing_if = "Option::is_none", default)]
#[serde(rename = "type")] #[serde(rename = "type")]
@ -171,7 +106,7 @@ pub struct JsonSchemaBevyType {
/// Validation with "additionalProperties" applies only to the child /// 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". /// 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)] #[serde(skip_serializing_if = "Option::is_none", default)]
pub additional_properties: Option<bool>, pub additional_properties: Option<JsonSchemaVariant>,
/// Validation succeeds if, for each name that appears in both the instance and as a name /// 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 /// within this keyword's value, the child instance for that name successfully validates
/// against the corresponding schema. /// against the corresponding schema.
@ -202,12 +137,39 @@ pub struct JsonSchemaBevyType {
/// array elements have been evaluated against this keyword's subschema. /// array elements have been evaluated against this keyword's subschema.
#[serde(skip_serializing_if = "Option::is_none", default)] #[serde(skip_serializing_if = "Option::is_none", default)]
pub items: Option<JsonSchemaVariant>, pub items: Option<JsonSchemaVariant>,
/// 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<u64>,
/// 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<u64>,
/// 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)] #[serde(skip_serializing_if = "Option::is_none", default)]
pub minimum: Option<SchemaNumber>, pub minimum: Option<SchemaNumber>,
/// 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)] #[serde(skip_serializing_if = "Option::is_none", default)]
pub maximum: Option<SchemaNumber>, pub maximum: Option<SchemaNumber>,
/// 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)] #[serde(skip_serializing_if = "Option::is_none", default)]
pub exclusive_minimum: Option<SchemaNumber>, pub exclusive_minimum: Option<SchemaNumber>,
/// 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)] #[serde(skip_serializing_if = "Option::is_none", default)]
pub exclusive_maximum: Option<SchemaNumber>, pub exclusive_maximum: Option<SchemaNumber>,
/// Type description /// Type description
@ -215,20 +177,52 @@ pub struct JsonSchemaBevyType {
pub description: Option<String>, pub description: Option<String>,
} }
/// 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)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Reflect)]
#[serde(untagged)] #[serde(untagged)]
pub enum JsonSchemaVariant { 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), 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 { Const {
/// The constant value that must be matched exactly.
#[reflect(ignore)] #[reflect(ignore)]
#[serde(rename = "const")] #[serde(rename = "const")]
value: Value, value: Value,
/// Optional human-readable description of the constant value.
#[serde(skip_serializing_if = "Option::is_none", default)] #[serde(skip_serializing_if = "Option::is_none", default)]
description: Option<String>, description: Option<String>,
}, },
/// 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<JsonSchemaBevyType>), Schema(#[reflect(ignore)] Box<JsonSchemaBevyType>),
} }
impl JsonSchemaVariant { 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<String>) -> Self { pub fn const_value(serializable: impl Serialize, description: Option<String>) -> Self {
Self::Const { Self::Const {
value: serde_json::to_value(serializable).unwrap_or_default(), value: serde_json::to_value(serializable).unwrap_or_default(),
@ -264,10 +258,20 @@ pub enum SchemaKind {
/// Optional type /// Optional type
Optional, 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)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Reflect, Eq, PartialOrd, Ord)]
#[serde(untagged)] #[serde(untagged)]
pub enum SchemaTypeVariant { 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), 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<SchemaType>), Multiple(Vec<SchemaType>),
} }
@ -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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -407,6 +359,7 @@ mod tests {
use bevy_ecs::{component::Component, reflect::AppTypeRegistry, resource::Resource}; use bevy_ecs::{component::Component, reflect::AppTypeRegistry, resource::Resource};
use bevy_reflect::prelude::ReflectDefault; use bevy_reflect::prelude::ReflectDefault;
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use serde_json::json;
#[test] #[test]
fn reflect_export_struct() { fn reflect_export_struct() {
@ -654,8 +607,8 @@ mod tests {
"minimum": 0, "minimum": 0,
"type": "integer", "type": "integer",
"description": "Test doc", "description": "Test doc",
"shortPath": "", "shortPath": "u16",
"typePath": "", "typePath": "u16",
}, },
}, },

View File

@ -14,12 +14,12 @@ pub mod json_schema;
pub mod open_rpc; pub mod open_rpc;
pub mod reflect_info; 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. /// later on used in Bevy Json Schema.
#[derive(Debug, Resource, Reflect)] #[derive(Debug, Resource, Reflect)]
#[reflect(Resource)] #[reflect(Resource)]
pub struct SchemaTypesMetadata { pub struct SchemaTypesMetadata {
/// Type Data id mapping to strings. /// Type Data id mapping to human-readable type names.
pub type_data_map: HashMap<TypeId, String>, pub type_data_map: HashMap<TypeId, String>,
} }
@ -42,7 +42,7 @@ impl Default for SchemaTypesMetadata {
} }
impl SchemaTypesMetadata { impl SchemaTypesMetadata {
/// Map `TypeId` of `TypeData` to string /// Map `TypeId` of `TypeData` to a human-readable type name
pub fn map_type_data<T: TypeData>(&mut self, name: impl Into<String>) { pub fn map_type_data<T: TypeData>(&mut self, name: impl Into<String>) {
self.type_data_map.insert(TypeId::of::<T>(), name.into()); self.type_data_map.insert(TypeId::of::<T>(), name.into());
} }
@ -55,12 +55,12 @@ impl SchemaTypesMetadata {
.collect() .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<T: TypeData>(&self, types_string_slice: &[String]) -> bool { pub fn has_type_data<T: TypeData>(&self, types_string_slice: &[String]) -> bool {
self.has_type_data_by_id(TypeId::of::<T>(), types_string_slice) self.has_type_data_by_id(TypeId::of::<T>(), 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 { pub fn has_type_data_by_id(&self, id: TypeId, types_string_slice: &[String]) -> bool {
self.type_data_map self.type_data_map
.get(&id) .get(&id)

View File

@ -1,12 +1,9 @@
//! Module containing information about reflected types. //! Module containing information about reflected types.
use bevy_reflect::{ use bevy_reflect::{GenericInfo, NamedField, Reflect, TypeInfo, UnnamedField, VariantInfo};
GenericInfo, NamedField, Reflect, StructVariantInfo, TypeInfo, UnnamedField, VariantInfo,
};
use core::any::TypeId; use core::any::TypeId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
any::Any, any::Any,
f32::consts::PI,
fmt::Debug, fmt::Debug,
ops::{Bound, RangeBounds}, ops::{Bound, RangeBounds},
}; };
@ -16,12 +13,12 @@ use crate::schemas::json_schema::{
}; };
/// Enum representing a number in 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)] #[serde(untagged)]
pub enum SchemaNumber { pub enum SchemaNumber {
/// Integer value. /// Integer value.
Int(i128), Int(i128),
/// Always finite. /// Floating-point value.
Float(f64), Float(f64),
} }
@ -86,16 +83,74 @@ impl From<isize> for SchemaNumber {
SchemaNumber::Int(value as i128) 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<SchemaNumber> {
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<SchemaNumber> {
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)] #[derive(Clone, Debug, Default, Serialize, Deserialize, Copy, PartialEq, Reflect)]
pub struct MinMaxValues { pub struct MinMaxValues {
pub min: Option<SchemaNumber>, /// The minimum bound value, if any.
pub min_exclusive: Option<SchemaNumber>, pub min: Option<BoundValue>,
pub max: Option<SchemaNumber>, /// The maximum bound value, if any.
pub max_exclusive: Option<SchemaNumber>, pub max: Option<BoundValue>,
} }
impl MinMaxValues { 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<T, Y>(reflect_val: &dyn Reflect) -> Option<MinMaxValues> pub fn from_reflect<T, Y>(reflect_val: &dyn Reflect) -> Option<MinMaxValues>
where where
T: 'static + RangeBounds<Y>, T: 'static + RangeBounds<Y>,
@ -110,112 +165,172 @@ 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<T>(value: (Bound<&T>, Bound<&T>, TypeId)) -> MinMaxValues pub fn from_range<T>(value: (Bound<&T>, Bound<&T>, TypeId)) -> MinMaxValues
where where
T: 'static + Into<SchemaNumber> + Copy + Debug, T: 'static + Into<SchemaNumber> + Copy + Debug,
{ {
let base: MinMaxValues = value.2.into(); let base: MinMaxValues = value.2.into();
let (min, min_exclusive) = match value.0 { let min = match value.0 {
Bound::Included(v) => (Some((*v).into()), None), Bound::Included(v) => Some(BoundValue::Inclusive((*v).into())),
Bound::Excluded(v) => (None, Some((*v).into())), Bound::Excluded(v) => Some(BoundValue::Exclusive((*v).into())),
Bound::Unbounded => (base.min, None), Bound::Unbounded => base.min,
}; };
let (max, max_exclusive) = match value.1 { let max = match value.1 {
Bound::Included(v) => (Some((*v).into()), None), Bound::Included(v) => Some(BoundValue::Inclusive((*v).into())),
Bound::Excluded(v) => (None, Some((*v).into())), Bound::Excluded(v) => Some(BoundValue::Exclusive((*v).into())),
Bound::Unbounded => (base.max, None), Bound::Unbounded => base.max,
}; };
Self { Self { min, max }
min,
min_exclusive,
max,
max_exclusive,
}
} }
} }
impl From<TypeId> for MinMaxValues { impl From<TypeId> for MinMaxValues {
fn from(value: TypeId) -> Self { fn from(value: TypeId) -> Self {
let mut min: Option<SchemaNumber> = None; let mut min: Option<BoundValue> = None;
let mut max: Option<SchemaNumber> = None; let mut max: Option<BoundValue> = None;
if value.eq(&TypeId::of::<u8>()) { if value.eq(&TypeId::of::<u8>()) {
min = Some(0.into()); min = Some(BoundValue::Inclusive(0.into()));
max = Some(u8::MAX.into()); max = Some(BoundValue::Inclusive(u8::MAX.into()));
} else if value.eq(&TypeId::of::<u16>()) { } else if value.eq(&TypeId::of::<u16>()) {
min = Some(0.into()); min = Some(BoundValue::Inclusive(0.into()));
max = Some(u16::MAX.into()); max = Some(BoundValue::Inclusive(u16::MAX.into()));
} else if value.eq(&TypeId::of::<u32>()) { } else if value.eq(&TypeId::of::<u32>()) {
min = Some(0.into()); min = Some(BoundValue::Inclusive(0.into()));
max = Some(u32::MAX.into()); max = Some(BoundValue::Inclusive(u32::MAX.into()));
} else if value.eq(&TypeId::of::<u64>()) { } else if value.eq(&TypeId::of::<u64>()) {
min = Some(0.into()); min = Some(BoundValue::Inclusive(0.into()));
} else if value.eq(&TypeId::of::<u128>()) { } else if value.eq(&TypeId::of::<u128>()) {
min = Some(0.into()); min = Some(BoundValue::Inclusive(0.into()));
} else if value.eq(&TypeId::of::<usize>()) { } else if value.eq(&TypeId::of::<usize>()) {
min = Some(0.into()); min = Some(BoundValue::Inclusive(0.into()));
} else if value.eq(&TypeId::of::<i8>()) { } else if value.eq(&TypeId::of::<i8>()) {
min = Some(i8::MIN.into()); min = Some(BoundValue::Inclusive(i8::MIN.into()));
max = Some(i8::MAX.into()); max = Some(BoundValue::Inclusive(i8::MAX.into()));
} else if value.eq(&TypeId::of::<i16>()) { } else if value.eq(&TypeId::of::<i16>()) {
min = Some(i16::MIN.into()); min = Some(BoundValue::Inclusive(i16::MIN.into()));
max = Some(i16::MAX.into()); max = Some(BoundValue::Inclusive(i16::MAX.into()));
} else if value.eq(&TypeId::of::<i32>()) { } else if value.eq(&TypeId::of::<i32>()) {
min = Some(i32::MIN.into()); min = Some(BoundValue::Inclusive(i32::MIN.into()));
max = Some(i32::MAX.into()); max = Some(BoundValue::Inclusive(i32::MAX.into()));
} }
MinMaxValues { MinMaxValues { min, max }
min,
max,
min_exclusive: None,
max_exclusive: None,
} }
} }
} /// 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)] #[derive(Clone, Debug, Default)]
pub enum InternalSchemaType { pub enum InternalSchemaType {
Primitive { /// Represents array-like types (Vec, arrays, lists, sets).
range: MinMaxValues, Array {
schema_type: SchemaType, /// The TypeId of the element type contained in the array.
element_type: TypeId,
/// Optional type information for the element type.
element_type_info: Option<TypeInfo>,
/// Minimum number of elements allowed in the array.
min_size: Option<u64>,
/// Maximum number of elements allowed in the array.
max_size: Option<u64>,
}, },
/// Holds all variants of an enum type.
EnumHolder(Vec<VariantInfo>),
/// Represents a single enum variant.
EnumVariant(VariantInfo), EnumVariant(VariantInfo),
/// Holds named fields for struct types.
NamedFieldsHolder(Vec<NamedField>), NamedFieldsHolder(Vec<NamedField>),
/// Holds unnamed fields for tuple and tuple struct types.
UnnamedFieldsHolder(Vec<UnnamedField>), UnnamedFieldsHolder(Vec<UnnamedField>),
/// Represents an Optional type (e.g., Option<T>).
Optional { Optional {
/// Generic information about the wrapped type T in Option<T>.
generic: GenericInfo, generic: GenericInfo,
range: MinMaxValues,
schema_type: SchemaType,
}, },
/// Represents a Map type (e.g., HashMap<K, V>).
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<TypeInfo>,
/// The TypeId of the value type contained in the map.
value_type: TypeId,
/// Optional type information for the value type.
value_type_info: Option<TypeInfo>,
},
/// Default variant for regular primitive types and other simple types.
#[default] #[default]
Regular, Regular,
} }
impl From<&SchemaTypeInfo> for Option<SchemaTypeVariant> {
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)] #[derive(Clone, Debug, Default)]
pub struct SchemaTypeInfo { pub struct SchemaTypeInfo {
/// The internal categorization of the schema type.
pub internal_type: InternalSchemaType, pub internal_type: InternalSchemaType,
/// Optional documentation string extracted from the type.
pub documentation: Option<String>, pub documentation: Option<String>,
/// The kind of schema (struct, enum, value, etc.).
pub kind: SchemaKind, pub kind: SchemaKind,
/// Optional Bevy reflection type information.
pub type_info: Option<TypeInfo>, pub type_info: Option<TypeInfo>,
/// Optional TypeId for the type.
pub type_id: Option<TypeId>,
/// Numeric range constraints for the type.
pub range: MinMaxValues,
} }
impl Into<JsonSchemaVariant> for SchemaTypeInfo { impl Into<JsonSchemaVariant> for SchemaTypeInfo {
fn into(self) -> JsonSchemaVariant { fn into(self) -> JsonSchemaVariant {
match self.internal_type { let schema_type: Option<SchemaTypeVariant> = (&self).into();
InternalSchemaType::Primitive { range, schema_type } => { let mut schema = JsonSchemaBevyType {
JsonSchemaVariant::Schema(Box::new(JsonSchemaBevyType { schema_type: schema_type.clone(),
kind: self.kind, kind: self.kind.clone(),
schema_type: Some(SchemaTypeVariant::Single(schema_type)), description: self.documentation.clone(),
minimum: range.min,
maximum: range.max,
exclusive_minimum: range.min_exclusive,
exclusive_maximum: range.max_exclusive,
description: self.documentation,
..Default::default()
}))
}
InternalSchemaType::Regular => {
JsonSchemaVariant::Schema(Box::new(JsonSchemaBevyType {
type_path: self type_path: self
.type_info .type_info
.as_ref() .as_ref()
@ -234,175 +349,149 @@ impl Into<JsonSchemaVariant> for SchemaTypeInfo {
.type_info .type_info
.as_ref() .as_ref()
.and_then(|s| s.type_path_table().module_path().map(str::to_owned)), .and_then(|s| s.type_path_table().module_path().map(str::to_owned)),
description: self.documentation, minimum: self.range.min.as_ref().and_then(|r| r.get_inclusive()),
kind: self.kind, 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() ..Default::default()
})) };
match self.internal_type {
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 => {}
InternalSchemaType::EnumHolder(variants) => {
schema.one_of = variants.iter().map(|v| v.build_schema()).collect();
} }
InternalSchemaType::EnumVariant(variant_info) => match &variant_info { InternalSchemaType::EnumVariant(variant_info) => match &variant_info {
VariantInfo::Struct(struct_variant_info) => { VariantInfo::Struct(struct_variant_info) => {
schema.kind = SchemaKind::Value;
schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Object));
let internal_type = InternalSchemaType::NamedFieldsHolder( let internal_type = InternalSchemaType::NamedFieldsHolder(
struct_variant_info.iter().cloned().collect(), struct_variant_info.iter().cloned().collect(),
); );
let schema = SchemaTypeInfo { let schema_field = SchemaTypeInfo {
internal_type, internal_type,
documentation: variant_info.to_description(), documentation: variant_info.to_description(),
kind: SchemaKind::Struct, kind: SchemaKind::Struct,
type_info: None, type_info: None,
type_id: Some(struct_variant_info.type_id()),
range: MinMaxValues::default(),
}; };
JsonSchemaVariant::Schema(Box::new(JsonSchemaBevyType {
description: self.documentation, schema.properties =
kind: SchemaKind::Value, [(variant_info.name().to_string(), schema_field.into())].into();
schema_type: Some(SchemaTypeVariant::Single(SchemaType::Object)), schema.required = vec![variant_info.name().to_string()];
properties: [(variant_info.name().to_string(), schema.into())].into(),
required: vec![variant_info.name().to_string()],
..Default::default()
}))
} }
VariantInfo::Tuple(tuple_variant_info) => { VariantInfo::Tuple(tuple_variant_info) => {
schema.kind = SchemaKind::Value;
schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Object));
let internal_type = InternalSchemaType::UnnamedFieldsHolder( let internal_type = InternalSchemaType::UnnamedFieldsHolder(
tuple_variant_info.iter().cloned().collect(), tuple_variant_info.iter().cloned().collect(),
); );
let schema = SchemaTypeInfo { let schema_field = SchemaTypeInfo {
internal_type, internal_type,
documentation: variant_info.to_description(), documentation: variant_info.to_description(),
kind: SchemaKind::Tuple, kind: SchemaKind::Tuple,
type_info: None, type_info: None,
type_id: Some(tuple_variant_info.type_id()),
range: MinMaxValues::default(),
}; };
JsonSchemaVariant::Schema(Box::new(JsonSchemaBevyType { schema.properties =
description: self.documentation, [(variant_info.name().to_string(), schema_field.into())].into();
kind: SchemaKind::Value, schema.required = vec![variant_info.name().to_string()];
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()
}))
} }
VariantInfo::Unit(unit_variant_info) => JsonSchemaVariant::const_value( VariantInfo::Unit(unit_variant_info) => {
return JsonSchemaVariant::const_value(
unit_variant_info.name().to_string(), unit_variant_info.name().to_string(),
self.documentation, schema.description.clone(),
), );
}
}, },
InternalSchemaType::NamedFieldsHolder(named_fields) => { InternalSchemaType::NamedFieldsHolder(named_fields) => {
JsonSchemaVariant::Schema(Box::new(JsonSchemaBevyType { schema.additional_properties = Some(JsonSchemaVariant::BoolValue(false));
kind: self.kind, schema.schema_type = Some(SchemaTypeVariant::Single(SchemaType::Object));
schema_type: Some(SchemaTypeVariant::Single(SchemaType::Object)), schema.properties = named_fields
description: self.documentation,
additional_properties: Some(false),
properties: named_fields
.iter() .iter()
.map(|field| (field.name().to_string(), field.build_schema())) .map(|field| (field.name().to_string(), field.build_schema()))
.collect(), .collect();
required: named_fields schema.required = named_fields
.iter() .iter()
.map(|field| field.name().to_string()) .map(|field| field.name().to_string())
.collect(), .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()
}))
} }
InternalSchemaType::UnnamedFieldsHolder(unnamed_fields) => { InternalSchemaType::UnnamedFieldsHolder(unnamed_fields) => {
if unnamed_fields.len() == 1 { if unnamed_fields.len() == 1 {
let s = unnamed_fields[0].build_schema(); let new_schema = unnamed_fields[0].build_schema();
if let JsonSchemaVariant::Schema(mut schema) = s { if let JsonSchemaVariant::Schema(new_schema_type) = new_schema {
schema.kind = self.kind; schema = *new_schema_type;
schema.description = self.documentation; schema.schema_type = schema_type.clone();
schema.description = self.documentation.clone();
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)
} else { } else {
s return new_schema;
} }
} else { } else {
JsonSchemaVariant::Schema(Box::new(JsonSchemaBevyType { schema.prefix_items = unnamed_fields.iter().map(|s| s.build_schema()).collect();
description: self.documentation, schema.min_items = Some(unnamed_fields.len() as u64);
kind: self.kind, schema.max_items = Some(unnamed_fields.len() as u64);
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()
}))
} }
} }
InternalSchemaType::Optional { InternalSchemaType::Array {
generic, element_type,
range, element_type_info,
schema_type, 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(); let schema_variant = generic.ty().id().build_schema();
if let JsonSchemaVariant::Schema(mut value) = schema_variant { if let JsonSchemaVariant::Schema(value) = schema_variant {
value.minimum = range.min; schema = *value;
value.maximum = range.max; schema.schema_type = schema_type.clone();
value.exclusive_minimum = range.min_exclusive; schema.minimum = self.range.min.as_ref().and_then(|r| r.get_inclusive());
value.exclusive_maximum = range.max_exclusive; schema.maximum = self.range.max.as_ref().and_then(|r| r.get_inclusive());
value.description = self.documentation; schema.exclusive_minimum =
value.kind = SchemaKind::Optional; self.range.min.as_ref().and_then(|r| r.get_exclusive());
value.schema_type = Some(SchemaTypeVariant::Multiple(vec![ schema.exclusive_maximum =
schema_type, self.range.max.as_ref().and_then(|r| r.get_exclusive());
SchemaType::Null, schema.description = self.documentation;
])); schema.kind = SchemaKind::Optional;
JsonSchemaVariant::Schema(value)
} else { } else {
schema_variant return schema_variant;
} }
} }
} }
JsonSchemaVariant::Schema(Box::new(schema))
} }
} }
/// Trait that builds the type information based on the reflected data. /// Trait that builds the type information based on the reflected data.
pub trait SchemaInfoReflect { pub trait SchemaInfoReflect {
/// Returns the optional type information of the schema.
fn try_get_optional_info(&self) -> Option<GenericInfo> { fn try_get_optional_info(&self) -> Option<GenericInfo> {
let type_info = self.try_get_type_info()?; let type_info = self.try_get_type_info()?;
let TypeInfo::Enum(enum_info) = type_info else { let TypeInfo::Enum(enum_info) = type_info else {
@ -422,6 +511,28 @@ pub trait SchemaInfoReflect {
fn try_get_type_info(&self) -> Option<TypeInfo>; fn try_get_type_info(&self) -> Option<TypeInfo>;
/// Returns the Bevy kind of the schema. /// Returns the Bevy kind of the schema.
fn get_kind(&self) -> SchemaKind { 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 SchemaKind::Value
} }
/// Builds the type information based on the reflected data. /// Builds the type information based on the reflected data.
@ -436,18 +547,15 @@ pub trait SchemaInfoReflect {
internal_type, internal_type,
documentation: self.to_description(), documentation: self.to_description(),
kind: self.get_kind(), 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 { fn build_internal_type(&self) -> InternalSchemaType {
if let Some(generic) = self.try_get_optional_info() { if let Some(generic) = self.try_get_optional_info() {
let range = self.get_range_by_id(); return InternalSchemaType::Optional { generic };
let schema_type: SchemaType = generic.ty().id().into();
return InternalSchemaType::Optional {
generic,
range,
schema_type,
};
} }
if let Some(type_info) = self.try_get_type_info() { if let Some(type_info) = self.try_get_type_info() {
match type_info { match type_info {
@ -466,28 +574,49 @@ pub trait SchemaInfoReflect {
tuple_info.iter().cloned().collect(), 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::List(list_info) => {
// TypeInfo::Array(array_info) => todo!(), return InternalSchemaType::Array {
// TypeInfo::Map(map_info) => todo!(), element_type: list_info.item_ty().id(),
// TypeInfo::Set(set_info) => todo!(), 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!(), // 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. /// Builds the description based on the reflected data.
fn to_description(&self) -> Option<String> { fn to_description(&self) -> Option<String> {
@ -508,6 +637,9 @@ pub trait SchemaInfoReflect {
None 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<T, Y>(&self) -> Option<MinMaxValues> fn min_max_from_attribute<T, Y>(&self) -> Option<MinMaxValues>
where where
T: 'static + RangeBounds<Y>, T: 'static + RangeBounds<Y>,
@ -517,6 +649,9 @@ pub trait SchemaInfoReflect {
.and_then(|reflect_value| MinMaxValues::from_reflect::<T, Y>(reflect_value)) .and_then(|reflect_value| MinMaxValues::from_reflect::<T, Y>(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<T>(&self) -> Option<MinMaxValues> fn min_max_from_attribute_for_type<T>(&self) -> Option<MinMaxValues>
where where
T: 'static + Into<SchemaNumber> + Copy + Debug, T: 'static + Into<SchemaNumber> + Copy + Debug,
@ -548,6 +683,9 @@ pub trait SchemaInfoReflect {
None 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 { fn get_range_by_id(&self) -> MinMaxValues {
let t = match self.try_get_optional_info() { let t = match self.try_get_optional_info() {
Some(info) => info.ty().id(), Some(info) => info.ty().id(),
@ -569,6 +707,8 @@ pub trait SchemaInfoReflect {
self.min_max_from_attribute_for_type::<u32>() self.min_max_from_attribute_for_type::<u32>()
} else if t.eq(&TypeId::of::<i32>()) { } else if t.eq(&TypeId::of::<i32>()) {
self.min_max_from_attribute_for_type::<i32>() self.min_max_from_attribute_for_type::<i32>()
} else if t.eq(&TypeId::of::<i64>()) {
self.min_max_from_attribute_for_type::<i64>()
} else if t.eq(&TypeId::of::<u64>()) { } else if t.eq(&TypeId::of::<u64>()) {
self.min_max_from_attribute_for_type::<u64>() self.min_max_from_attribute_for_type::<u64>()
} else if t.eq(&TypeId::of::<i64>()) { } else if t.eq(&TypeId::of::<i64>()) {
@ -648,19 +788,6 @@ impl SchemaInfoReflect for TypeInfo {
fn try_get_type_info(&self) -> Option<TypeInfo> { fn try_get_type_info(&self) -> Option<TypeInfo> {
Some(self.clone()) 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")] #[cfg(feature = "documentation")]
fn get_docs(&self) -> Option<&str> { fn get_docs(&self) -> Option<&str> {
self.docs() self.docs()
@ -685,6 +812,7 @@ impl SchemaInfoReflect for TypeId {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use bevy_platform::collections::HashMap;
use bevy_reflect::GetTypeRegistration; use bevy_reflect::GetTypeRegistration;
use super::*; use super::*;
@ -692,12 +820,16 @@ mod tests {
#[test] #[test]
fn integer_test() { fn integer_test() {
let type_info = TypeId::of::<u16>().build_schema_type_info(); let type_info = TypeId::of::<u16>().build_schema_type_info();
let InternalSchemaType::Primitive { range, schema_type } = type_info.internal_type else { let schema_type: Option<SchemaTypeVariant> = (&type_info).into();
return; assert_eq!(type_info.range.min, Some(BoundValue::Inclusive(0.into())));
}; assert_eq!(
assert_eq!(range.min, Some(0.into())); type_info.range.max,
assert_eq!(range.max, Some(u16::MAX.into())); Some(BoundValue::Inclusive(u16::MAX.into()))
assert_eq!(schema_type, SchemaType::Integer); );
assert_eq!(
schema_type,
Some(SchemaTypeVariant::Single(SchemaType::Integer))
);
} }
#[test] #[test]
@ -714,12 +846,13 @@ mod tests {
.expect("Should not fail"); .expect("Should not fail");
let field_info = struct_info.field("no_value").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 type_info = field_info.build_schema_type_info();
let InternalSchemaType::Primitive { range, schema_type } = type_info.internal_type else { let schema_type: Option<SchemaTypeVariant> = (&type_info).into();
return; assert_eq!(type_info.range.min, Some(BoundValue::Inclusive(10.into())));
}; assert_eq!(type_info.range.max, Some(BoundValue::Inclusive(13.into())));
assert_eq!(range.min, Some(10.into())); assert_eq!(
assert_eq!(range.max, Some(13.into())); schema_type,
assert_eq!(schema_type, SchemaType::Integer); Some(SchemaTypeVariant::Single(SchemaType::Integer))
);
assert_eq!( assert_eq!(
type_info.documentation, type_info.documentation,
Some("Test documentation".to_string()) Some("Test documentation".to_string())
@ -749,14 +882,17 @@ mod tests {
.expect("Should not fail"); .expect("Should not fail");
let field_info = struct_info.field("no_value").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 type_info = field_info.build_schema_type_info();
let InternalSchemaType::Primitive { range, schema_type } = type_info.internal_type else { let schema_type: Option<SchemaTypeVariant> = (&type_info).into();
return; assert!(!type_info.range.in_range((-1).into()));
}; assert!(type_info.range.in_range(0.into()));
eprintln!("Range: {:#?}", range); assert!(type_info.range.in_range(12.into()));
assert_eq!(range.min, Some(0.into())); assert!(!type_info.range.in_range(13.into()));
assert_eq!(range.max, None); assert_eq!(type_info.range.min, Some(BoundValue::Inclusive(0.into())));
assert_eq!(range.max_exclusive, Some(13.into())); assert_eq!(type_info.range.max, Some(BoundValue::Exclusive(13.into())));
assert_eq!(schema_type, SchemaType::Integer); assert_eq!(
schema_type,
Some(SchemaTypeVariant::Single(SchemaType::Integer))
);
assert_eq!( assert_eq!(
type_info.documentation, type_info.documentation,
Some("Test documentation".to_string()) Some("Test documentation".to_string())
@ -786,13 +922,13 @@ mod tests {
.expect("Should not fail"); .expect("Should not fail");
let field_info = struct_info.iter().next().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 type_info = field_info.build_schema_type_info();
let InternalSchemaType::Primitive { range, schema_type } = type_info.internal_type else { let schema_type: Option<SchemaTypeVariant> = (&type_info).into();
return; assert_eq!(type_info.range.min, Some(BoundValue::Inclusive(0.into())));
}; assert_eq!(type_info.range.max, Some(BoundValue::Exclusive(13.into())));
assert_eq!(range.min, Some(0.into())); assert_eq!(
assert_eq!(range.max, None); schema_type,
assert_eq!(range.max_exclusive, Some(13.into())); Some(SchemaTypeVariant::Single(SchemaType::Integer))
assert_eq!(schema_type, SchemaType::Integer); );
assert_eq!( assert_eq!(
type_info.documentation, type_info.documentation,
Some("Test documentation".to_string()) Some("Test documentation".to_string())
@ -814,20 +950,71 @@ mod tests {
Variant4(usize), Variant4(usize),
} }
eprintln!( 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() #[test]
.expect("Should not fail"); fn reflect_struct_with_array() {
for field in enum_info.iter() { #[derive(Reflect, Default, Deserialize, Serialize)]
let type_info = field.build_schema(); pub struct ArrayComponent {
pub arry: [i32; 3],
}
eprintln!( eprintln!(
"{}: {}", "{}",
field.name(), serde_json::to_string_pretty(
serde_json::to_string_pretty(&type_info).unwrap() &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<i32, Option<i32>>,
}
assert!(serde_json::from_str::<HashMapStruct>(
"{\"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("")
); );
} }
} }
}