Path: blob/main/crates/bevy_remote/src/schemas/json_schema.rs
6849 views
//! Module with JSON Schema type for Bevy Registry Types.1//! It tries to follow this standard: <https://json-schema.org/specification>2use alloc::borrow::Cow;3use bevy_platform::collections::HashMap;4use bevy_reflect::{5GetTypeRegistration, NamedField, OpaqueInfo, TypeInfo, TypeRegistration, TypeRegistry,6VariantInfo,7};8use core::any::TypeId;9use serde::{Deserialize, Serialize};10use serde_json::{json, Map, Value};1112use crate::schemas::SchemaTypesMetadata;1314/// Helper trait for converting `TypeRegistration` to `JsonSchemaBevyType`15pub trait TypeRegistrySchemaReader {16/// Export type JSON Schema.17fn export_type_json_schema<T: GetTypeRegistration + 'static>(18&self,19extra_info: &SchemaTypesMetadata,20) -> Option<JsonSchemaBevyType> {21self.export_type_json_schema_for_id(extra_info, TypeId::of::<T>())22}23/// Export type JSON Schema.24fn export_type_json_schema_for_id(25&self,26extra_info: &SchemaTypesMetadata,27type_id: TypeId,28) -> Option<JsonSchemaBevyType>;29}3031impl TypeRegistrySchemaReader for TypeRegistry {32fn export_type_json_schema_for_id(33&self,34extra_info: &SchemaTypesMetadata,35type_id: TypeId,36) -> Option<JsonSchemaBevyType> {37let type_reg = self.get(type_id)?;38Some((type_reg, extra_info).into())39}40}4142/// Exports schema info for a given type43pub fn export_type(44reg: &TypeRegistration,45metadata: &SchemaTypesMetadata,46) -> (Cow<'static, str>, JsonSchemaBevyType) {47(reg.type_info().type_path().into(), (reg, metadata).into())48}4950impl From<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType {51fn from(value: (&TypeRegistration, &SchemaTypesMetadata)) -> Self {52let (reg, metadata) = value;53let t = reg.type_info();54let binding = t.type_path_table();5556let short_path = binding.short_path();57let type_path = binding.path();58let mut typed_schema = JsonSchemaBevyType {59reflect_types: metadata.get_registered_reflect_types(reg),60short_path: short_path.to_owned(),61type_path: type_path.to_owned(),62crate_name: binding.crate_name().map(str::to_owned),63module_path: binding.module_path().map(str::to_owned),64..Default::default()65};66match t {67TypeInfo::Struct(info) => {68typed_schema.properties = info69.iter()70.map(|field| (field.name().to_owned(), field.ty().ref_type()))71.collect::<HashMap<_, _>>();72typed_schema.required = info73.iter()74.filter(|field| !field.type_path().starts_with("core::option::Option"))75.map(|f| f.name().to_owned())76.collect::<Vec<_>>();77typed_schema.additional_properties = Some(false);78typed_schema.schema_type = SchemaType::Object;79typed_schema.kind = SchemaKind::Struct;80}81TypeInfo::Enum(info) => {82typed_schema.kind = SchemaKind::Enum;8384let simple = info85.iter()86.all(|variant| matches!(variant, VariantInfo::Unit(_)));87if simple {88typed_schema.schema_type = SchemaType::String;89typed_schema.one_of = info90.iter()91.map(|variant| match variant {92VariantInfo::Unit(v) => v.name().into(),93_ => unreachable!(),94})95.collect::<Vec<_>>();96} else {97typed_schema.schema_type = SchemaType::Object;98typed_schema.one_of = info99.iter()100.map(|variant| match variant {101VariantInfo::Struct(v) => json!({102"type": "object",103"kind": "Struct",104"typePath": format!("{}::{}", type_path, v.name()),105"shortPath": v.name(),106"properties": v107.iter()108.map(|field| (field.name().to_owned(), field.ref_type()))109.collect::<Map<_, _>>(),110"additionalProperties": false,111"required": v112.iter()113.filter(|field| !field.type_path().starts_with("core::option::Option"))114.map(NamedField::name)115.collect::<Vec<_>>(),116}),117VariantInfo::Tuple(v) => json!({118"type": "array",119"kind": "Tuple",120"typePath": format!("{}::{}", type_path, v.name()),121"shortPath": v.name(),122"prefixItems": v123.iter()124.map(SchemaJsonReference::ref_type)125.collect::<Vec<_>>(),126"items": false,127}),128VariantInfo::Unit(v) => json!({129"typePath": format!("{}::{}", type_path, v.name()),130"shortPath": v.name(),131}),132})133.collect::<Vec<_>>();134}135}136TypeInfo::TupleStruct(info) => {137typed_schema.schema_type = SchemaType::Array;138typed_schema.kind = SchemaKind::TupleStruct;139typed_schema.prefix_items = info140.iter()141.map(SchemaJsonReference::ref_type)142.collect::<Vec<_>>();143typed_schema.items = Some(false.into());144}145TypeInfo::List(info) => {146typed_schema.schema_type = SchemaType::Array;147typed_schema.kind = SchemaKind::List;148typed_schema.items = info.item_ty().ref_type().into();149}150TypeInfo::Array(info) => {151typed_schema.schema_type = SchemaType::Array;152typed_schema.kind = SchemaKind::Array;153typed_schema.items = info.item_ty().ref_type().into();154}155TypeInfo::Map(info) => {156typed_schema.schema_type = SchemaType::Object;157typed_schema.kind = SchemaKind::Map;158typed_schema.key_type = info.key_ty().ref_type().into();159typed_schema.value_type = info.value_ty().ref_type().into();160}161TypeInfo::Tuple(info) => {162typed_schema.schema_type = SchemaType::Array;163typed_schema.kind = SchemaKind::Tuple;164typed_schema.prefix_items = info165.iter()166.map(SchemaJsonReference::ref_type)167.collect::<Vec<_>>();168typed_schema.items = Some(false.into());169}170TypeInfo::Set(info) => {171typed_schema.schema_type = SchemaType::Set;172typed_schema.kind = SchemaKind::Set;173typed_schema.items = info.value_ty().ref_type().into();174}175TypeInfo::Opaque(info) => {176typed_schema.schema_type = info.map_json_type();177typed_schema.kind = SchemaKind::Value;178}179};180typed_schema181}182}183184/// JSON Schema type for Bevy Registry Types185/// It tries to follow this standard: <https://json-schema.org/specification>186///187/// To take the full advantage from info provided by Bevy registry it provides extra fields188#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]189#[serde(rename_all = "camelCase")]190pub struct JsonSchemaBevyType {191/// Bevy specific field, short path of the type.192pub short_path: String,193/// Bevy specific field, full path of the type.194pub type_path: String,195/// Bevy specific field, path of the module that type is part of.196#[serde(skip_serializing_if = "Option::is_none", default)]197pub module_path: Option<String>,198/// Bevy specific field, name of the crate that type is part of.199#[serde(skip_serializing_if = "Option::is_none", default)]200pub crate_name: Option<String>,201/// Bevy specific field, names of the types that type reflects.202#[serde(skip_serializing_if = "Vec::is_empty", default)]203pub reflect_types: Vec<String>,204/// Bevy specific field, [`TypeInfo`] type mapping.205pub kind: SchemaKind,206/// Bevy specific field, provided when [`SchemaKind`] `kind` field is equal to [`SchemaKind::Map`].207///208/// It contains type info of key of the Map.209#[serde(skip_serializing_if = "Option::is_none", default)]210pub key_type: Option<Value>,211/// Bevy specific field, provided when [`SchemaKind`] `kind` field is equal to [`SchemaKind::Map`].212///213/// It contains type info of value of the Map.214#[serde(skip_serializing_if = "Option::is_none", default)]215pub value_type: Option<Value>,216/// The type keyword is fundamental to JSON Schema. It specifies the data type for a schema.217#[serde(rename = "type")]218pub schema_type: SchemaType,219/// The behavior of this keyword depends on the presence and annotation results of "properties"220/// and "patternProperties" within the same schema object.221/// Validation with "additionalProperties" applies only to the child222/// values of instance names that do not appear in the annotation results of either "properties" or "patternProperties".223#[serde(skip_serializing_if = "Option::is_none", default)]224pub additional_properties: Option<bool>,225/// Validation succeeds if, for each name that appears in both the instance and as a name226/// within this keyword's value, the child instance for that name successfully validates227/// against the corresponding schema.228#[serde(skip_serializing_if = "HashMap::is_empty", default)]229pub properties: HashMap<String, Value>,230/// An object instance is valid against this keyword if every item in the array is the name of a property in the instance.231#[serde(skip_serializing_if = "Vec::is_empty", default)]232pub required: Vec<String>,233/// An instance validates successfully against this keyword if it validates successfully against exactly one schema defined by this keyword's value.234#[serde(skip_serializing_if = "Vec::is_empty", default)]235pub one_of: Vec<Value>,236/// Validation succeeds if each element of the instance validates against the schema at the same position, if any. This keyword does not constrain the length of the array. If the array is longer than this keyword's value, this keyword validates only the prefix of matching length.237///238/// This keyword produces an annotation value which is the largest index to which this keyword239/// applied a subschema. The value MAY be a boolean true if a subschema was applied to every240/// index of the instance, such as is produced by the "items" keyword.241/// This annotation affects the behavior of "items" and "unevaluatedItems".242#[serde(skip_serializing_if = "Vec::is_empty", default)]243pub prefix_items: Vec<Value>,244/// This keyword applies its subschema to all instance elements at indexes greater245/// than the length of the "prefixItems" array in the same schema object,246/// as reported by the annotation result of that "prefixItems" keyword.247/// If no such annotation result exists, "items" applies its subschema to all248/// instance array elements.249///250/// If the "items" subschema is applied to any positions within the instance array,251/// it produces an annotation result of boolean true, indicating that all remaining252/// array elements have been evaluated against this keyword's subschema.253#[serde(skip_serializing_if = "Option::is_none", default)]254pub items: Option<Value>,255}256257/// Kind of json schema, maps [`TypeInfo`] type258#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]259pub enum SchemaKind {260/// Struct261#[default]262Struct,263/// Enum type264Enum,265/// A key-value map266Map,267/// Array268Array,269/// List270List,271/// Fixed size collection of items272Tuple,273/// Fixed size collection of items with named fields274TupleStruct,275/// Set of unique values276Set,277/// Single value, eg. primitive types278Value,279}280281/// Type of json schema282#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]283#[serde(rename_all = "lowercase")]284pub enum SchemaType {285/// Represents a string value.286String,287288/// Represents a floating-point number.289Float,290291/// Represents an unsigned integer.292Uint,293294/// Represents a signed integer.295Int,296297/// Represents an object with key-value pairs.298Object,299300/// Represents an array of values.301Array,302303/// Represents a boolean value (true or false).304Boolean,305306/// Represents a set of unique values.307Set,308309/// Represents a null value.310#[default]311Null,312}313314/// Helper trait for generating json schema reference315trait SchemaJsonReference {316/// Reference to another type in schema.317/// The value `$ref` is a URI-reference that is resolved against the schema.318fn ref_type(self) -> Value;319}320321/// Helper trait for mapping bevy type path into json schema type322pub trait SchemaJsonType {323/// Bevy Reflect type path324fn get_type_path(&self) -> &'static str;325326/// JSON Schema type keyword from Bevy reflect type path into327fn map_json_type(&self) -> SchemaType {328match self.get_type_path() {329"bool" => SchemaType::Boolean,330"u8" | "u16" | "u32" | "u64" | "u128" | "usize" => SchemaType::Uint,331"i8" | "i16" | "i32" | "i64" | "i128" | "isize" => SchemaType::Int,332"f32" | "f64" => SchemaType::Float,333"char" | "str" | "alloc::string::String" => SchemaType::String,334_ => SchemaType::Object,335}336}337}338339impl SchemaJsonType for OpaqueInfo {340fn get_type_path(&self) -> &'static str {341self.type_path()342}343}344345impl SchemaJsonReference for &bevy_reflect::Type {346fn ref_type(self) -> Value {347let path = self.path();348json!({"type": json!({ "$ref": format!("#/$defs/{path}") })})349}350}351352impl SchemaJsonReference for &bevy_reflect::UnnamedField {353fn ref_type(self) -> Value {354let path = self.type_path();355json!({"type": json!({ "$ref": format!("#/$defs/{path}") })})356}357}358359impl SchemaJsonReference for &NamedField {360fn ref_type(self) -> Value {361let type_path = self.type_path();362json!({"type": json!({ "$ref": format!("#/$defs/{type_path}") }), "typePath": self.name()})363}364}365366#[cfg(test)]367mod tests {368use super::*;369use bevy_ecs::prelude::ReflectComponent;370use bevy_ecs::prelude::ReflectResource;371372use bevy_ecs::{component::Component, reflect::AppTypeRegistry, resource::Resource};373use bevy_reflect::prelude::ReflectDefault;374use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};375376#[test]377fn reflect_export_struct() {378#[derive(Reflect, Resource, Default, Deserialize, Serialize)]379#[reflect(Resource, Default, Serialize, Deserialize)]380struct Foo {381a: f32,382b: Option<f32>,383}384385let atr = AppTypeRegistry::default();386{387let mut register = atr.write();388register.register::<Foo>();389}390let type_registry = atr.read();391let foo_registration = type_registry392.get(TypeId::of::<Foo>())393.expect("SHOULD BE REGISTERED")394.clone();395let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());396397assert!(398!schema.reflect_types.contains(&"Component".to_owned()),399"Should not be a component"400);401assert!(402schema.reflect_types.contains(&"Resource".to_owned()),403"Should be a resource"404);405let _ = schema.properties.get("a").expect("Missing `a` field");406let _ = schema.properties.get("b").expect("Missing `b` field");407assert!(408schema.required.contains(&"a".to_owned()),409"Field a should be required"410);411assert!(412!schema.required.contains(&"b".to_owned()),413"Field b should not be required"414);415}416417#[test]418fn reflect_export_enum() {419#[derive(Reflect, Component, Default, Deserialize, Serialize)]420#[reflect(Component, Default, Serialize, Deserialize)]421enum EnumComponent {422ValueOne(i32),423ValueTwo {424test: i32,425},426#[default]427NoValue,428}429430let atr = AppTypeRegistry::default();431{432let mut register = atr.write();433register.register::<EnumComponent>();434}435let type_registry = atr.read();436let foo_registration = type_registry437.get(TypeId::of::<EnumComponent>())438.expect("SHOULD BE REGISTERED")439.clone();440let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());441assert!(442schema.reflect_types.contains(&"Component".to_owned()),443"Should be a component"444);445assert!(446!schema.reflect_types.contains(&"Resource".to_owned()),447"Should not be a resource"448);449assert!(schema.properties.is_empty(), "Should not have any field");450assert!(schema.one_of.len() == 3, "Should have 3 possible schemas");451}452453#[test]454fn reflect_export_struct_without_reflect_types() {455#[derive(Reflect, Component, Default, Deserialize, Serialize)]456enum EnumComponent {457ValueOne(i32),458ValueTwo {459test: i32,460},461#[default]462NoValue,463}464465let atr = AppTypeRegistry::default();466{467let mut register = atr.write();468register.register::<EnumComponent>();469}470let type_registry = atr.read();471let foo_registration = type_registry472.get(TypeId::of::<EnumComponent>())473.expect("SHOULD BE REGISTERED")474.clone();475let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());476assert!(477!schema.reflect_types.contains(&"Component".to_owned()),478"Should not be a component"479);480assert!(481!schema.reflect_types.contains(&"Resource".to_owned()),482"Should not be a resource"483);484assert!(schema.properties.is_empty(), "Should not have any field");485assert!(schema.one_of.len() == 3, "Should have 3 possible schemas");486}487488#[test]489fn reflect_struct_with_custom_type_data() {490#[derive(Reflect, Default, Deserialize, Serialize)]491#[reflect(Default)]492enum EnumComponent {493ValueOne(i32),494ValueTwo {495test: i32,496},497#[default]498NoValue,499}500501#[derive(Clone)]502pub struct ReflectCustomData;503504impl<T: Reflect> bevy_reflect::FromType<T> for ReflectCustomData {505fn from_type() -> Self {506ReflectCustomData507}508}509510let atr = AppTypeRegistry::default();511{512let mut register = atr.write();513register.register::<EnumComponent>();514register.register_type_data::<EnumComponent, ReflectCustomData>();515}516let mut metadata = SchemaTypesMetadata::default();517metadata.map_type_data::<ReflectCustomData>("CustomData");518let type_registry = atr.read();519let foo_registration = type_registry520.get(TypeId::of::<EnumComponent>())521.expect("SHOULD BE REGISTERED")522.clone();523let (_, schema) = export_type(&foo_registration, &metadata);524assert!(525!metadata.has_type_data::<ReflectComponent>(&schema.reflect_types),526"Should not be a component"527);528assert!(529!metadata.has_type_data::<ReflectResource>(&schema.reflect_types),530"Should not be a resource"531);532assert!(533metadata.has_type_data::<ReflectDefault>(&schema.reflect_types),534"Should have default"535);536assert!(537metadata.has_type_data::<ReflectCustomData>(&schema.reflect_types),538"Should have CustomData"539);540assert!(schema.properties.is_empty(), "Should not have any field");541assert!(schema.one_of.len() == 3, "Should have 3 possible schemas");542}543544#[test]545fn reflect_export_tuple_struct() {546#[derive(Reflect, Component, Default, Deserialize, Serialize)]547#[reflect(Component, Default, Serialize, Deserialize)]548struct TupleStructType(usize, i32);549550let atr = AppTypeRegistry::default();551{552let mut register = atr.write();553register.register::<TupleStructType>();554}555let type_registry = atr.read();556let foo_registration = type_registry557.get(TypeId::of::<TupleStructType>())558.expect("SHOULD BE REGISTERED")559.clone();560let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());561assert!(562schema.reflect_types.contains(&"Component".to_owned()),563"Should be a component"564);565assert!(566!schema.reflect_types.contains(&"Resource".to_owned()),567"Should not be a resource"568);569assert!(schema.properties.is_empty(), "Should not have any field");570assert!(schema.prefix_items.len() == 2, "Should have 2 prefix items");571}572573#[test]574fn reflect_export_serialization_check() {575#[derive(Reflect, Resource, Default, Deserialize, Serialize)]576#[reflect(Resource, Default)]577struct Foo {578a: f32,579}580581let atr = AppTypeRegistry::default();582{583let mut register = atr.write();584register.register::<Foo>();585}586let type_registry = atr.read();587let foo_registration = type_registry588.get(TypeId::of::<Foo>())589.expect("SHOULD BE REGISTERED")590.clone();591let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());592let schema_as_value = serde_json::to_value(&schema).expect("Should serialize");593let value = json!({594"shortPath": "Foo",595"typePath": "bevy_remote::schemas::json_schema::tests::Foo",596"modulePath": "bevy_remote::schemas::json_schema::tests",597"crateName": "bevy_remote",598"reflectTypes": [599"Resource",600"Default",601],602"kind": "Struct",603"type": "object",604"additionalProperties": false,605"properties": {606"a": {607"type": {608"$ref": "#/$defs/f32"609}610},611},612"required": [613"a"614]615});616assert_normalized_values(schema_as_value, value);617}618619/// This function exist to avoid false failures due to ordering differences between `serde_json` values.620fn assert_normalized_values(mut one: Value, mut two: Value) {621normalize_json(&mut one);622normalize_json(&mut two);623assert_eq!(one, two);624625/// Recursively sorts arrays in a `serde_json::Value`626fn normalize_json(value: &mut Value) {627match value {628Value::Array(arr) => {629for v in arr.iter_mut() {630normalize_json(v);631}632arr.sort_by_key(ToString::to_string); // Sort by stringified version633}634Value::Object(map) => {635for (_k, v) in map.iter_mut() {636normalize_json(v);637}638}639_ => {}640}641}642}643}644645646