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>
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::<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
}
}
@ -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<Value>,
pub key_type: Option<JsonSchemaVariant>,
/// 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<Value>,
pub value_type: Option<JsonSchemaVariant>,
/// 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<bool>,
pub additional_properties: Option<JsonSchemaVariant>,
/// 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<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)]
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)]
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)]
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)]
pub exclusive_maximum: Option<SchemaNumber>,
/// Type description
@ -215,20 +177,52 @@ pub struct JsonSchemaBevyType {
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)]
#[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<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>),
}
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 {
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<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)]
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",
},
},

View File

@ -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<TypeId, String>,
}
@ -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<T: TypeData>(&mut self, name: impl Into<String>) {
self.type_data_map.insert(TypeId::of::<T>(), 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<T: TypeData>(&self, types_string_slice: &[String]) -> bool {
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 {
self.type_data_map
.get(&id)

View File

@ -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<isize> 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<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)]
pub struct MinMaxValues {
pub min: Option<SchemaNumber>,
pub min_exclusive: Option<SchemaNumber>,
pub max: Option<SchemaNumber>,
pub max_exclusive: Option<SchemaNumber>,
/// The minimum bound value, if any.
pub min: Option<BoundValue>,
/// The maximum bound value, if any.
pub max: Option<BoundValue>,
}
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>
where
T: 'static + RangeBounds<Y>,
@ -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<T>(value: (Bound<&T>, Bound<&T>, TypeId)) -> MinMaxValues
where
T: 'static + Into<SchemaNumber> + 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<TypeId> for MinMaxValues {
fn from(value: TypeId) -> Self {
let mut min: Option<SchemaNumber> = None;
let mut max: Option<SchemaNumber> = None;
let mut min: Option<BoundValue> = None;
let mut max: Option<BoundValue> = None;
if value.eq(&TypeId::of::<u8>()) {
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::<u16>()) {
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::<u32>()) {
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::<u64>()) {
min = Some(0.into());
min = Some(BoundValue::Inclusive(0.into()));
} else if value.eq(&TypeId::of::<u128>()) {
min = Some(0.into());
min = Some(BoundValue::Inclusive(0.into()));
} else if value.eq(&TypeId::of::<usize>()) {
min = Some(0.into());
min = Some(BoundValue::Inclusive(0.into()));
} else if value.eq(&TypeId::of::<i8>()) {
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::<i16>()) {
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::<i32>()) {
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<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),
/// Holds named fields for struct types.
NamedFieldsHolder(Vec<NamedField>),
/// Holds unnamed fields for tuple and tuple struct types.
UnnamedFieldsHolder(Vec<UnnamedField>),
/// Represents an Optional type (e.g., Option<T>).
Optional {
/// Generic information about the wrapped type T in Option<T>.
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]
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)]
pub struct SchemaTypeInfo {
/// The internal categorization of the schema type.
pub internal_type: InternalSchemaType,
/// Optional documentation string extracted from the type.
pub documentation: Option<String>,
/// The kind of schema (struct, enum, value, etc.).
pub kind: SchemaKind,
/// Optional Bevy reflection type information.
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 {
fn into(self) -> JsonSchemaVariant {
let schema_type: Option<SchemaTypeVariant> = (&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<GenericInfo> {
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<TypeInfo>;
/// 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<T, Y>(&self) -> Option<MinMaxValues>
where
T: 'static + RangeBounds<Y>,
@ -517,6 +649,9 @@ pub trait SchemaInfoReflect {
.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>
where
T: 'static + Into<SchemaNumber> + 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::<u32>()
} else if t.eq(&TypeId::of::<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>()) {
self.min_max_from_attribute_for_type::<u64>()
} else if t.eq(&TypeId::of::<i64>()) {
@ -648,19 +788,6 @@ impl SchemaInfoReflect for TypeInfo {
fn try_get_type_info(&self) -> Option<TypeInfo> {
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::<u16>().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<SchemaTypeVariant> = (&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<SchemaTypeVariant> = (&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<SchemaTypeVariant> = (&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<SchemaTypeVariant> = (&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<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("")
);
}
}