schema IDs using URN format, further improvements, tests for export
endpoint
This commit is contained in:
parent
16c7db42aa
commit
9ed0787d41
@ -26,8 +26,9 @@ use serde_json::{Map, Value};
|
||||
use crate::{
|
||||
error_codes,
|
||||
schemas::{
|
||||
json_schema::{JsonSchemaBevyType, TypeRegistrySchemaReader},
|
||||
json_schema::{JsonSchemaBevyType, SchemaMarker, TypeRegistrySchemaReader},
|
||||
open_rpc::OpenRpcDocument,
|
||||
reflect_info::TypeInformation,
|
||||
},
|
||||
BrpError, BrpResult,
|
||||
};
|
||||
@ -362,6 +363,22 @@ pub struct BrpJsonSchemaQueryFilter {
|
||||
pub type_limit: JsonSchemaTypeLimit,
|
||||
}
|
||||
|
||||
impl BrpJsonSchemaQueryFilter {
|
||||
/// Check if the filter should skip a type registration based on the crate name.
|
||||
pub fn should_skip_for_crate(&self, type_registration: &TypeRegistration) -> bool {
|
||||
let Some(crate_name) = type_registration.type_info().type_path_table().crate_name() else {
|
||||
return false;
|
||||
};
|
||||
if !self.with_crates.is_empty() && !self.with_crates.iter().any(|c| crate_name.eq(c)) {
|
||||
return true;
|
||||
}
|
||||
if !self.without_crates.is_empty() && self.without_crates.iter().any(|c| crate_name.eq(c)) {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Additional [`BrpJsonSchemaQueryFilter`] constraints that can be placed on a query to include or exclude
|
||||
/// certain definitions.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
|
||||
@ -375,6 +392,34 @@ pub struct JsonSchemaTypeLimit {
|
||||
pub with: Vec<String>,
|
||||
}
|
||||
|
||||
impl JsonSchemaTypeLimit {
|
||||
/// Check if the type limit is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.without.is_empty() && self.with.is_empty()
|
||||
}
|
||||
|
||||
/// Check if the type limit should skip a type.
|
||||
pub fn should_skip_type(&self, registered_types: &[Cow<'_, str>]) -> bool {
|
||||
if !self.with.is_empty()
|
||||
&& !self
|
||||
.with
|
||||
.iter()
|
||||
.any(|c| registered_types.iter().any(|cc| c.eq(cc)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if !self.without.is_empty()
|
||||
&& self
|
||||
.without
|
||||
.iter()
|
||||
.any(|c| registered_types.iter().any(|cc| c.eq(cc)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// A response from the world to the client that specifies a single entity.
|
||||
///
|
||||
/// This is sent in response to `bevy/spawn`.
|
||||
@ -1230,48 +1275,47 @@ pub fn export_registry_types(In(params): In<Option<Value>>, world: &World) -> Br
|
||||
let extra_info = world.resource::<crate::schemas::SchemaTypesMetadata>();
|
||||
let types = world.resource::<AppTypeRegistry>();
|
||||
let types = types.read();
|
||||
let schemas = types
|
||||
let mut schema = JsonSchemaBevyType {
|
||||
schema: Some(SchemaMarker.into()),
|
||||
description: Some(
|
||||
"This schema represents the types registered in the Bevy application.".into(),
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
schema.definitions = types
|
||||
.iter()
|
||||
.filter_map(|type_reg| {
|
||||
let path_table = type_reg.type_info().type_path_table();
|
||||
if let Some(crate_name) = &path_table.crate_name() {
|
||||
if !filter.with_crates.is_empty()
|
||||
&& !filter.with_crates.iter().any(|c| crate_name.eq(c))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if !filter.without_crates.is_empty()
|
||||
&& filter.without_crates.iter().any(|c| crate_name.eq(c))
|
||||
if filter.should_skip_for_crate(type_reg) {
|
||||
return None;
|
||||
}
|
||||
if !filter.type_limit.is_empty() {
|
||||
let registered_types = extra_info.get_registered_reflect_types(type_reg);
|
||||
if filter
|
||||
.type_limit
|
||||
.should_skip_type(registered_types.as_slice())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let id = type_reg.type_id();
|
||||
let schema = types.export_type_json_schema_for_id(id, extra_info)?;
|
||||
|
||||
if !filter.type_limit.with.is_empty()
|
||||
&& !filter
|
||||
.type_limit
|
||||
.with
|
||||
.iter()
|
||||
.any(|c| schema.reflect_type_data.iter().any(|cc| c.eq(cc)))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if !filter.type_limit.without.is_empty()
|
||||
&& filter
|
||||
.type_limit
|
||||
.without
|
||||
.iter()
|
||||
.any(|c| schema.reflect_type_data.iter().any(|cc| c.eq(cc)))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some((type_reg.type_info().type_path().into(), schema))
|
||||
let schema_type_info = TypeInformation::TypeRegistration(type_reg.clone())
|
||||
.to_schema_type_info_with_metadata(extra_info);
|
||||
let schema_id = schema_type_info.ty_info.try_get_type_reference_id()?;
|
||||
let mut definition = schema_type_info.to_definition();
|
||||
definition.schema.default_value = types.try_get_default_value_for_type_id(id);
|
||||
definition.schema.schema = None;
|
||||
let mut definitions = vec![(schema_id, Box::new(definition.schema))];
|
||||
let extra_defs = definition.definitions.iter().map(|(id, definition)| {
|
||||
let def = definition.to_definition();
|
||||
(id.clone(), Box::new(def.schema))
|
||||
});
|
||||
definitions.extend(extra_defs);
|
||||
Some(definitions)
|
||||
})
|
||||
.collect::<HashMap<Cow<'static, str>, JsonSchemaBevyType>>();
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
serde_json::to_value(schemas).map_err(BrpError::internal)
|
||||
serde_json::to_value(schema).map_err(BrpError::internal)
|
||||
}
|
||||
|
||||
/// Immutably retrieves an entity from the [`World`], returning an error if the
|
||||
@ -1533,6 +1577,12 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
use bevy_ecs::component::Component;
|
||||
use bevy_math::Vec3;
|
||||
use bevy_reflect::Reflect;
|
||||
|
||||
use crate::schemas::{ReflectJsonSchemaForceAsArray, SchemaTypesMetadata};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@ -1556,4 +1606,96 @@ mod tests {
|
||||
entity: Entity::from_raw_u32(0).unwrap(),
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_schema_test() {
|
||||
#[derive(Reflect, Default, Deserialize, Serialize)]
|
||||
pub struct OtherStruct {
|
||||
/// FIELD DOC
|
||||
pub field: String,
|
||||
}
|
||||
/// STRUCT DOC
|
||||
#[derive(Reflect, Default, Deserialize, Serialize)]
|
||||
pub struct SecondStruct {
|
||||
pub field: String,
|
||||
/// FIELD DOC
|
||||
pub other: OtherStruct,
|
||||
}
|
||||
/// STRUCT DOC
|
||||
#[derive(Reflect, Default, Deserialize, Serialize)]
|
||||
pub struct ThirdStruct {
|
||||
pub array_strings: Vec<String>,
|
||||
pub array_structs: [OtherStruct; 5],
|
||||
pub map_strings: HashMap<String, i32>,
|
||||
}
|
||||
#[derive(Reflect, Default, Deserialize, Serialize, Component)]
|
||||
#[reflect(Component)]
|
||||
pub struct NestedStruct {
|
||||
pub other: OtherStruct,
|
||||
pub second: SecondStruct,
|
||||
/// DOC FOR FIELD
|
||||
pub third: ThirdStruct,
|
||||
}
|
||||
|
||||
let mut world = World::new();
|
||||
let atr = AppTypeRegistry::default();
|
||||
{
|
||||
let mut register = atr.write();
|
||||
register.register::<NestedStruct>();
|
||||
register.register::<Vec3>();
|
||||
register.register_type_data::<Vec3, ReflectJsonSchemaForceAsArray>();
|
||||
}
|
||||
world.insert_resource(atr);
|
||||
world.insert_resource(SchemaTypesMetadata::default());
|
||||
let response = export_registry_types_ext(BrpJsonSchemaQueryFilter::default(), &world);
|
||||
|
||||
assert_eq!(response.definitions.len(), 5);
|
||||
let response = export_registry_types_ext(
|
||||
BrpJsonSchemaQueryFilter {
|
||||
without_crates: vec!["bevy_remote".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
&world,
|
||||
);
|
||||
assert_eq!(response.definitions.len(), 1);
|
||||
{
|
||||
let first = response.definitions.iter().next().expect("Should have one");
|
||||
assert_eq!(first.1.id, "urn:bevy:glam-Vec3");
|
||||
}
|
||||
let response = export_registry_types_ext(
|
||||
BrpJsonSchemaQueryFilter {
|
||||
with_crates: vec!["bevy_remote".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
&world,
|
||||
);
|
||||
assert_eq!(response.definitions.len(), 4);
|
||||
let response = export_registry_types_ext(
|
||||
BrpJsonSchemaQueryFilter {
|
||||
type_limit: JsonSchemaTypeLimit {
|
||||
with: vec!["Component".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
&world,
|
||||
);
|
||||
assert_eq!(response.definitions.len(), 4);
|
||||
}
|
||||
|
||||
fn export_registry_types_ext(
|
||||
input: BrpJsonSchemaQueryFilter,
|
||||
world: &World,
|
||||
) -> JsonSchemaBevyType {
|
||||
let response_json = export_registry_types(
|
||||
In(Some(
|
||||
serde_json::to_value(&input).expect("Failed to serialize input"),
|
||||
)),
|
||||
world,
|
||||
)
|
||||
.expect("Failed to export registry types");
|
||||
|
||||
serde_json::from_value::<JsonSchemaBevyType>(response_json)
|
||||
.expect("Failed to deserialize response")
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ use serde_json::Value;
|
||||
|
||||
use crate::schemas::{
|
||||
reflect_info::{SchemaNumber, TypeInformation, TypeReferenceId, TypeReferencePath},
|
||||
ReflectJsonSchema, SchemaTypesMetadata,
|
||||
SchemaTypesMetadata,
|
||||
};
|
||||
|
||||
/// Helper trait for converting `TypeRegistration` to `JsonSchemaBevyType`
|
||||
@ -43,7 +43,7 @@ impl TypeRegistrySchemaReader for TypeRegistry {
|
||||
) -> Option<JsonSchemaBevyType> {
|
||||
let type_reg = self.get(type_id)?;
|
||||
let mut schema: JsonSchemaBevyType = (type_reg, extra_info).try_into().ok()?;
|
||||
schema.schema = Some(SchemaMarker::default());
|
||||
schema.schema = Some(SchemaMarker.into());
|
||||
schema.default_value = self.try_get_default_value_for_type_id(type_id);
|
||||
|
||||
Some(schema)
|
||||
@ -79,11 +79,11 @@ impl TryFrom<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType {
|
||||
|
||||
fn try_from(value: (&TypeRegistration, &SchemaTypesMetadata)) -> Result<Self, Self::Error> {
|
||||
let (reg, metadata) = value;
|
||||
if let Some(s) = reg.data::<ReflectJsonSchema>() {
|
||||
return Ok(s.0.clone());
|
||||
}
|
||||
// if let Some(s) = reg.data::<ReflectJsonSchema>() {
|
||||
// return Ok(s.0.clone());
|
||||
// }
|
||||
let mut schema: JsonSchemaBevyType = TypeInformation::from(reg)
|
||||
.to_schema_type_info()
|
||||
.to_schema_type_info_with_metadata(metadata)
|
||||
.to_definition()
|
||||
.into();
|
||||
schema.reflect_type_data = metadata.get_registered_reflect_types(reg);
|
||||
@ -101,11 +101,21 @@ impl TryFrom<&TypeRegistration> for JsonSchemaBevyType {
|
||||
|
||||
/// Identifies the JSON Schema version used in the schema.
|
||||
#[derive(Deserialize, Serialize, Debug, Reflect, PartialEq, Clone)]
|
||||
pub struct SchemaMarker(Cow<'static, str>);
|
||||
pub struct SchemaMarker;
|
||||
|
||||
impl Default for SchemaMarker {
|
||||
fn default() -> Self {
|
||||
Self("https://json-schema.org/draft/2020-12/schema".into())
|
||||
impl SchemaMarker {
|
||||
const DEFAULT_SCHEMA: &'static str = "https://json-schema.org/draft/2020-12/schema";
|
||||
}
|
||||
|
||||
impl From<SchemaMarker> for &'static str {
|
||||
fn from(_: SchemaMarker) -> Self {
|
||||
SchemaMarker::DEFAULT_SCHEMA
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SchemaMarker> for Cow<'static, str> {
|
||||
fn from(_: SchemaMarker) -> Self {
|
||||
Cow::Borrowed(SchemaMarker::DEFAULT_SCHEMA)
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,10 +126,15 @@ impl Default for SchemaMarker {
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default, Reflect)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct JsonSchemaBevyType {
|
||||
/// JSON Schema specific field.
|
||||
/// This keyword declares an identifier for the schema resource.
|
||||
#[serde(skip_serializing_if = "str::is_empty", default)]
|
||||
#[serde(rename = "$id")]
|
||||
pub id: Cow<'static, str>,
|
||||
/// Identifies the JSON Schema version used in the schema.
|
||||
#[serde(rename = "$schema")]
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub schema: Option<SchemaMarker>,
|
||||
pub schema: Option<Cow<'static, str>>,
|
||||
/// JSON Schema specific field.
|
||||
/// This keyword is used to reference a statically identified schema.
|
||||
#[serde(rename = "$ref")]
|
||||
@ -245,7 +260,7 @@ pub struct JsonSchemaBevyType {
|
||||
#[serde(skip_serializing_if = "HashMap::is_empty", default)]
|
||||
#[reflect(ignore)]
|
||||
#[serde(rename = "$defs")]
|
||||
pub definitions: HashMap<TypeReferenceId, JsonSchemaVariant>,
|
||||
pub definitions: HashMap<TypeReferenceId, Box<JsonSchemaBevyType>>,
|
||||
}
|
||||
|
||||
/// Represents different types of JSON Schema values that can be used in schema definitions.
|
||||
@ -411,7 +426,9 @@ impl SchemaType {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::schemas::open_rpc::OpenRpcDocument;
|
||||
use crate::schemas::reflect_info::ReferenceLocation;
|
||||
use crate::schemas::ReflectJsonSchema;
|
||||
|
||||
use super::*;
|
||||
use bevy_ecs::prelude::ReflectComponent;
|
||||
@ -587,6 +604,35 @@ mod tests {
|
||||
assert!(schema.one_of.len() == 3, "Should have 3 possible schemas");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflect_struct_with_custom_schema() {
|
||||
let atr = AppTypeRegistry::default();
|
||||
{
|
||||
let mut register = atr.write();
|
||||
register.register::<OpenRpcDocument>();
|
||||
register.register_type_data::<OpenRpcDocument, ReflectJsonSchema>();
|
||||
}
|
||||
let type_registry = atr.read();
|
||||
let schema = type_registry
|
||||
.export_type_json_schema::<OpenRpcDocument>(&SchemaTypesMetadata::default())
|
||||
.expect("Failed to export schema");
|
||||
assert_eq!(
|
||||
schema.ref_type,
|
||||
Some(TypeReferencePath::new_ref(
|
||||
ReferenceLocation::Url,
|
||||
"raw.githubusercontent.com/open-rpc/meta-schema/master/schema.json",
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
schema.description,
|
||||
Some(
|
||||
"Represents an `OpenRPC` document as defined by the `OpenRPC` specification."
|
||||
.into()
|
||||
)
|
||||
);
|
||||
assert!(schema.properties.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflect_export_with_custom_schema() {
|
||||
#[derive(Reflect, Component)]
|
||||
@ -666,6 +712,7 @@ mod tests {
|
||||
let schema = export_type::<Foo>();
|
||||
let schema_as_value = serde_json::to_value(&schema).expect("Failed to serialize schema");
|
||||
let mut value = json!({
|
||||
"$id": "urn:bevy:bevy_remote-schemas-json_schema-tests-Foo",
|
||||
"shortPath": "Foo",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"typePath": "bevy_remote::schemas::json_schema::tests::Foo",
|
||||
|
@ -20,7 +20,7 @@ pub mod reflect_info;
|
||||
|
||||
/// Holds mapping of reflect [type data](TypeData) to human-readable type names,
|
||||
/// later on used in Bevy Json Schema.
|
||||
#[derive(Debug, Resource, Reflect)]
|
||||
#[derive(Debug, Resource, Reflect, Clone)]
|
||||
#[reflect(Resource)]
|
||||
pub struct SchemaTypesMetadata {
|
||||
/// Type Data id mapping to human-readable type names.
|
||||
|
@ -24,7 +24,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use crate::schemas::json_schema::{
|
||||
JsonSchemaBevyType, JsonSchemaVariant, SchemaKind, SchemaType, SchemaTypeVariant,
|
||||
};
|
||||
use crate::schemas::ReflectJsonSchemaForceAsArray;
|
||||
use crate::schemas::{ReflectJsonSchemaForceAsArray, SchemaTypesMetadata};
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
@ -90,6 +90,8 @@ pub enum FieldType {
|
||||
/// Unnamed field type.
|
||||
#[default]
|
||||
Unnamed,
|
||||
/// Named field type that is stored as unnamed. Example: glam Vec3.
|
||||
ForceUnnamed,
|
||||
}
|
||||
|
||||
/// Information about the attributes of a field.
|
||||
@ -109,7 +111,7 @@ impl From<&TypeInformation> for Option<FieldsInformation> {
|
||||
TypeInfo::Struct(struct_info) => (
|
||||
get_fields_information(struct_info.iter()),
|
||||
if value.is_forced_as_array() {
|
||||
FieldType::Unnamed
|
||||
FieldType::ForceUnnamed
|
||||
} else {
|
||||
FieldType::Named
|
||||
},
|
||||
@ -200,13 +202,34 @@ pub enum TypeInformation {
|
||||
}
|
||||
|
||||
impl TypeInformation {
|
||||
/// Returns the reference type if the stored type is a reference one.
|
||||
pub fn get_reference_type(&self) -> Option<TypeReferencePath> {
|
||||
if self.is_primitive_type() {
|
||||
None
|
||||
/// Checks for custom schema.
|
||||
pub fn try_get_custom_schema(&self) -> Option<&super::ReflectJsonSchema> {
|
||||
if let Self::TypeRegistration(reg) = self {
|
||||
reg.data::<super::ReflectJsonSchema>()
|
||||
} else {
|
||||
self.try_get_type_path_table()
|
||||
.map(TypeReferencePath::definition)
|
||||
None
|
||||
}
|
||||
}
|
||||
/// Builds a `TypeReferenceId` from the type path.
|
||||
pub fn try_get_type_reference_id(&self) -> Option<TypeReferenceId> {
|
||||
if let Some(schema) = self.try_get_custom_schema() {
|
||||
if schema.0.id.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(schema.0.id.trim().into())
|
||||
}
|
||||
} else if self.is_primitive_type() {
|
||||
None
|
||||
} else if let Some(optional) = self.try_get_optional_info() {
|
||||
Some(optional.type_path().into())
|
||||
} else if let Some(s) = self.try_get_type_info() {
|
||||
if s.as_array().is_ok() || s.as_list().is_ok() || s.as_map().is_ok() {
|
||||
None
|
||||
} else {
|
||||
self.try_get_type_path_table().map(|t| t.path().into())
|
||||
}
|
||||
} else {
|
||||
self.try_get_type_path_table().map(|t| t.path().into())
|
||||
}
|
||||
}
|
||||
/// Returns true if the stored type is a primitive one.
|
||||
@ -224,6 +247,25 @@ impl TypeInformation {
|
||||
ty_info: self,
|
||||
field_data: None,
|
||||
stored_fields,
|
||||
reflect_type_data: None,
|
||||
}
|
||||
}
|
||||
/// Converts the type information into a schema type information.
|
||||
pub fn to_schema_type_info_with_metadata(
|
||||
self,
|
||||
metadata: &SchemaTypesMetadata,
|
||||
) -> SchemaTypeInfo {
|
||||
let stored_fields = (&self).into();
|
||||
let reflect_type_data = if let Self::TypeRegistration(reg) = &self {
|
||||
Some(metadata.get_registered_reflect_types(reg))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
SchemaTypeInfo {
|
||||
ty_info: self,
|
||||
field_data: None,
|
||||
stored_fields,
|
||||
reflect_type_data,
|
||||
}
|
||||
}
|
||||
/// Returns the documentation of the type.
|
||||
@ -834,13 +876,14 @@ impl From<&TypeInformation> for InternalSchemaType {
|
||||
if let Some(type_info) = value.try_get_type_info() {
|
||||
match type_info {
|
||||
TypeInfo::Struct(struct_info) => {
|
||||
let fields = get_fields_information(struct_info.iter());
|
||||
let fields_type = if value.is_forced_as_array() {
|
||||
FieldType::Unnamed
|
||||
FieldType::ForceUnnamed
|
||||
} else {
|
||||
FieldType::Named
|
||||
};
|
||||
InternalSchemaType::FieldsHolder(FieldsInformation {
|
||||
fields: get_fields_information(struct_info.iter()),
|
||||
fields,
|
||||
fields_type,
|
||||
})
|
||||
}
|
||||
@ -883,7 +926,7 @@ impl From<&TypeInformation> for InternalSchemaType {
|
||||
key: (map_info.key_info(), &map_info.key_ty()).into(),
|
||||
value: (map_info.value_info(), &map_info.value_ty()).into(),
|
||||
},
|
||||
TypeInfo::Opaque(t) => InternalSchemaType::Regular(t.type_id()),
|
||||
TypeInfo::Opaque(t) => InternalSchemaType::RegularType(*t.ty()),
|
||||
}
|
||||
} else {
|
||||
match value {
|
||||
@ -925,18 +968,14 @@ impl From<&InternalSchemaType> for Option<SchemaTypeVariant> {
|
||||
VariantInfo::Struct(_) => Some(SchemaTypeVariant::Single(SchemaType::Object)),
|
||||
VariantInfo::Unit(_) => Some(SchemaTypeVariant::Single(SchemaType::String)),
|
||||
},
|
||||
InternalSchemaType::FieldsHolder(fields) => {
|
||||
if fields.fields_type == FieldType::Unnamed {
|
||||
if fields.fields.len() == 1 {
|
||||
let schema: InternalSchemaType = (&fields.fields[0].type_info).into();
|
||||
(&schema).into()
|
||||
} else {
|
||||
Some(SchemaTypeVariant::Single(SchemaType::Array))
|
||||
}
|
||||
} else {
|
||||
Some(SchemaTypeVariant::Single(SchemaType::Object))
|
||||
InternalSchemaType::FieldsHolder(fields) => match fields.fields_type {
|
||||
FieldType::Named => Some(SchemaTypeVariant::Single(SchemaType::Object)),
|
||||
FieldType::Unnamed if fields.fields.len() == 1 => {
|
||||
let schema: InternalSchemaType = (&fields.fields[0].type_info).into();
|
||||
(&schema).into()
|
||||
}
|
||||
}
|
||||
_ => Some(SchemaTypeVariant::Single(SchemaType::Array)),
|
||||
},
|
||||
InternalSchemaType::Optional {
|
||||
generic,
|
||||
schema_type_info: _,
|
||||
@ -972,23 +1011,11 @@ impl From<&FieldInformation> for SchemaTypeInfo {
|
||||
ty_info: value.type_info.clone(),
|
||||
field_data: Some(value.field.clone()),
|
||||
stored_fields: (&value.type_info).into(),
|
||||
reflect_type_data: 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 {
|
||||
/// Information about the type of the schema.
|
||||
pub ty_info: TypeInformation,
|
||||
/// Field information for the type.
|
||||
pub field_data: Option<SchemaFieldData>,
|
||||
/// Fields stored in the type.
|
||||
pub stored_fields: Option<FieldsInformation>,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@ -1015,6 +1042,21 @@ impl From<SchemaDefinition> for JsonSchemaBevyType {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// Information about the type of the schema.
|
||||
pub ty_info: TypeInformation,
|
||||
/// Field information for the type.
|
||||
pub field_data: Option<SchemaFieldData>,
|
||||
/// Fields stored in the type.
|
||||
pub stored_fields: Option<FieldsInformation>,
|
||||
/// Bevy specific field, names of the types that type reflects. Mapping of the names to the data types is provided by [`SchemaTypesMetadata`].
|
||||
pub reflect_type_data: Option<Vec<Cow<'static, str>>>,
|
||||
}
|
||||
|
||||
impl SchemaTypeInfo {
|
||||
/// Get the documentation for the schema type.
|
||||
/// If the field has a description, it is returned.
|
||||
@ -1052,7 +1094,12 @@ impl SchemaTypeInfo {
|
||||
let range = self.get_range();
|
||||
let description = self.get_docs();
|
||||
let internal_type: InternalSchemaType = (self).into();
|
||||
let (ref_type, schema_type) = (self.ty_info.get_reference_type(), self.into());
|
||||
let (ref_type, schema_type) = (
|
||||
self.ty_info
|
||||
.try_get_type_reference_id()
|
||||
.map(TypeReferencePath::definition),
|
||||
self.into(),
|
||||
);
|
||||
|
||||
let mut schema = JsonSchemaBevyType {
|
||||
description,
|
||||
@ -1076,6 +1123,7 @@ impl SchemaTypeInfo {
|
||||
ty_info: element_ty.clone(),
|
||||
field_data: None,
|
||||
stored_fields: None,
|
||||
reflect_type_data: None,
|
||||
};
|
||||
schema.items = Some(items_schema.to_ref_schema().into());
|
||||
schema.min_items = min_size;
|
||||
@ -1084,10 +1132,6 @@ impl SchemaTypeInfo {
|
||||
InternalSchemaType::EnumHolder(_)
|
||||
| InternalSchemaType::EnumVariant(_)
|
||||
| InternalSchemaType::FieldsHolder(_)
|
||||
| InternalSchemaType::Optional {
|
||||
generic: _,
|
||||
schema_type_info: _,
|
||||
}
|
||||
| InternalSchemaType::Map { key: _, value: _ } => {
|
||||
schema.ref_type = None;
|
||||
}
|
||||
@ -1099,9 +1143,15 @@ impl SchemaTypeInfo {
|
||||
|
||||
/// Converts the schema type information into a JSON schema definition.
|
||||
pub fn to_definition(&self) -> SchemaDefinition {
|
||||
let mut id: Option<TypeReferenceId> =
|
||||
self.ty_info.try_get_type_path_table().map(Into::into);
|
||||
let mut id: Option<TypeReferenceId> = self.ty_info.try_get_type_reference_id();
|
||||
let mut definitions: HashMap<TypeReferenceId, SchemaTypeInfo> = HashMap::new();
|
||||
if let Some(custom_schema) = &self.ty_info.try_get_custom_schema() {
|
||||
return SchemaDefinition {
|
||||
id,
|
||||
schema: custom_schema.0.clone(),
|
||||
definitions,
|
||||
};
|
||||
}
|
||||
let range = self.ty_info.get_range();
|
||||
|
||||
let (type_path, short_path, crate_name, module_path) =
|
||||
@ -1115,7 +1165,12 @@ impl SchemaTypeInfo {
|
||||
} else {
|
||||
(Cow::default(), Cow::default(), None, None)
|
||||
};
|
||||
let schema_id = id
|
||||
.as_ref()
|
||||
.map(|id| Cow::Owned(format!("urn:bevy:{}", id)))
|
||||
.unwrap_or_default();
|
||||
let mut schema = JsonSchemaBevyType {
|
||||
id: schema_id,
|
||||
description: self.ty_info.get_docs(),
|
||||
type_path,
|
||||
short_path,
|
||||
@ -1127,6 +1182,7 @@ impl SchemaTypeInfo {
|
||||
exclusive_minimum: range.min.get_exclusive(),
|
||||
exclusive_maximum: range.max.get_exclusive(),
|
||||
schema_type: self.into(),
|
||||
reflect_type_data: self.reflect_type_data.clone().unwrap_or_default(),
|
||||
..default()
|
||||
};
|
||||
let internal_type: InternalSchemaType = (self).into();
|
||||
@ -1136,11 +1192,13 @@ impl SchemaTypeInfo {
|
||||
ty_info: key.clone(),
|
||||
field_data: None,
|
||||
stored_fields: None,
|
||||
reflect_type_data: None,
|
||||
};
|
||||
let value: SchemaTypeInfo = SchemaTypeInfo {
|
||||
ty_info: value.clone(),
|
||||
field_data: None,
|
||||
stored_fields: None,
|
||||
reflect_type_data: None,
|
||||
};
|
||||
if !key.ty_info.is_primitive_type() {
|
||||
let SchemaDefinition {
|
||||
@ -1204,6 +1262,7 @@ impl SchemaTypeInfo {
|
||||
fields,
|
||||
fields_type: FieldType::Named,
|
||||
}),
|
||||
reflect_type_data: None,
|
||||
};
|
||||
let definition = schema_field.to_definition();
|
||||
|
||||
@ -1222,6 +1281,7 @@ impl SchemaTypeInfo {
|
||||
|
||||
fields_type: FieldType::Unnamed,
|
||||
}),
|
||||
reflect_type_data: None,
|
||||
};
|
||||
let definition = schema_field.to_definition();
|
||||
|
||||
@ -1235,6 +1295,7 @@ impl SchemaTypeInfo {
|
||||
ty_info,
|
||||
field_data,
|
||||
stored_fields: None,
|
||||
reflect_type_data: None,
|
||||
};
|
||||
return SchemaDefinition {
|
||||
id: None,
|
||||
@ -1272,11 +1333,11 @@ impl SchemaTypeInfo {
|
||||
schema: _,
|
||||
definitions: field_definitions,
|
||||
} = field_schema.to_definition();
|
||||
definitions.extend(field_definitions);
|
||||
let Some(id) = id else { continue };
|
||||
if !definitions.contains_key(&id) {
|
||||
definitions.insert(id, field_schema);
|
||||
}
|
||||
definitions.extend(field_definitions);
|
||||
}
|
||||
schema.required = fields
|
||||
.fields
|
||||
@ -1284,45 +1345,70 @@ impl SchemaTypeInfo {
|
||||
.map(|field| field.field.to_name())
|
||||
.collect();
|
||||
}
|
||||
FieldType::Unnamed => {
|
||||
if fields.fields.len() == 1 {
|
||||
let field_schema = SchemaTypeInfo::from(&fields.fields[0]);
|
||||
FieldType::Unnamed if fields.fields.len() == 1 => {
|
||||
let field_schema = SchemaTypeInfo::from(&fields.fields[0]);
|
||||
let SchemaDefinition {
|
||||
id,
|
||||
schema: new_schema_type,
|
||||
definitions: field_definitions,
|
||||
} = field_schema.to_definition();
|
||||
definitions.extend(field_definitions);
|
||||
if let Some(id) = id {
|
||||
definitions.insert(id, field_schema);
|
||||
}
|
||||
schema = new_schema_type;
|
||||
schema.schema_type = self.into();
|
||||
schema.description = self.get_docs();
|
||||
}
|
||||
s => {
|
||||
let schema_fields: Vec<SchemaTypeInfo> =
|
||||
fields.fields.iter().map(SchemaTypeInfo::from).collect();
|
||||
schema.prefix_items = schema_fields
|
||||
.iter()
|
||||
.map(|field| {
|
||||
let field_schema = if s == FieldType::ForceUnnamed
|
||||
&& field
|
||||
.field_data
|
||||
.as_ref()
|
||||
.is_some_and(|f| f.description.is_none())
|
||||
{
|
||||
if let Some(field_data) = field.field_data.as_ref() {
|
||||
let description = field_data.name.clone();
|
||||
SchemaTypeInfo {
|
||||
field_data: Some(SchemaFieldData {
|
||||
description,
|
||||
..field_data.clone()
|
||||
}),
|
||||
..field.clone()
|
||||
}
|
||||
.to_ref_schema()
|
||||
} else {
|
||||
field.to_ref_schema()
|
||||
}
|
||||
} else {
|
||||
field.to_ref_schema()
|
||||
};
|
||||
|
||||
field_schema.into()
|
||||
})
|
||||
.collect();
|
||||
for field_schema in schema_fields {
|
||||
if field_schema.ty_info.is_primitive_type() {
|
||||
continue;
|
||||
}
|
||||
let SchemaDefinition {
|
||||
id,
|
||||
schema: new_schema_type,
|
||||
definitions: _,
|
||||
schema: _,
|
||||
definitions: field_definitions,
|
||||
} = field_schema.to_definition();
|
||||
if let Some(id) = id {
|
||||
definitions.extend(field_definitions);
|
||||
let Some(id) = id else { continue };
|
||||
if !definitions.contains_key(&id) {
|
||||
definitions.insert(id, field_schema);
|
||||
}
|
||||
schema = new_schema_type;
|
||||
schema.schema_type = self.into();
|
||||
schema.description = self.get_docs();
|
||||
} else {
|
||||
let schema_fields: Vec<SchemaTypeInfo> =
|
||||
fields.fields.iter().map(SchemaTypeInfo::from).collect();
|
||||
schema.prefix_items = schema_fields
|
||||
.iter()
|
||||
.map(|field| field.to_ref_schema().into())
|
||||
.collect();
|
||||
for field_schema in schema_fields {
|
||||
if field_schema.ty_info.is_primitive_type() {
|
||||
continue;
|
||||
}
|
||||
let SchemaDefinition {
|
||||
id,
|
||||
schema: _,
|
||||
definitions: field_definitions,
|
||||
} = field_schema.to_definition();
|
||||
let Some(id) = id else { continue };
|
||||
if !definitions.contains_key(&id) {
|
||||
definitions.insert(id, field_schema);
|
||||
}
|
||||
definitions.extend(field_definitions);
|
||||
}
|
||||
schema.min_items = Some(fields.fields.len() as u64);
|
||||
schema.max_items = Some(fields.fields.len() as u64);
|
||||
}
|
||||
schema.min_items = Some(fields.fields.len() as u64);
|
||||
schema.max_items = Some(fields.fields.len() as u64);
|
||||
}
|
||||
},
|
||||
InternalSchemaType::Array {
|
||||
@ -1335,6 +1421,7 @@ impl SchemaTypeInfo {
|
||||
ty_info: element_ty.clone(),
|
||||
field_data: None,
|
||||
stored_fields: None,
|
||||
reflect_type_data: None,
|
||||
};
|
||||
schema.items = Some(items_schema.to_ref_schema().into());
|
||||
schema.min_items = min_size;
|
||||
@ -1346,9 +1433,9 @@ impl SchemaTypeInfo {
|
||||
schema: _,
|
||||
definitions: field_definitions,
|
||||
} = items_schema.to_definition();
|
||||
definitions.extend(field_definitions);
|
||||
if let Some(id) = id {
|
||||
definitions.insert(id, items_schema);
|
||||
definitions.extend(field_definitions);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1360,16 +1447,7 @@ impl SchemaTypeInfo {
|
||||
ty_info: TypeInformation::Type(Box::new(*generic.ty())),
|
||||
..(**schema_type_info).clone()
|
||||
};
|
||||
let optional_def = schema_optional.to_definition();
|
||||
let range = schema_optional.get_range();
|
||||
schema = optional_def.schema;
|
||||
schema.schema_type = self.into();
|
||||
schema.minimum = range.min.get_inclusive();
|
||||
schema.maximum = range.max.get_inclusive();
|
||||
schema.exclusive_minimum = range.min.get_exclusive();
|
||||
schema.exclusive_maximum = range.max.get_exclusive();
|
||||
schema.description = self.get_docs();
|
||||
schema.kind = Some(SchemaKind::Optional);
|
||||
return schema_optional.to_definition();
|
||||
}
|
||||
}
|
||||
SchemaDefinition {
|
||||
@ -1479,6 +1557,7 @@ where
|
||||
ty_info,
|
||||
field_data: Some(field_data),
|
||||
stored_fields,
|
||||
reflect_type_data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1762,6 +1841,38 @@ mod tests {
|
||||
let _: JsonSchemaBevyType = type_info.to_definition().into();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflect_multiple_definitions() {
|
||||
#[derive(Reflect, Default, Deserialize, Serialize)]
|
||||
pub struct BaseStruct {
|
||||
pub base_field: i32,
|
||||
pub second_field: i32,
|
||||
}
|
||||
#[derive(Reflect, Default, Deserialize, Serialize)]
|
||||
pub struct ArrayComponent {
|
||||
pub array: [BaseStruct; 3],
|
||||
}
|
||||
#[derive(Reflect, Default, Deserialize, Serialize)]
|
||||
pub struct ArrayComponentWithMoreVariants {
|
||||
pub array: [BaseStruct; 3],
|
||||
pub list: Vec<BaseStruct>,
|
||||
pub optional: Option<BaseStruct>,
|
||||
}
|
||||
let type_info = TypeInformation::from(&ArrayComponent::get_type_registration())
|
||||
.to_schema_type_info()
|
||||
.to_definition();
|
||||
let type_info_second =
|
||||
TypeInformation::from(&ArrayComponentWithMoreVariants::get_type_registration())
|
||||
.to_schema_type_info()
|
||||
.to_definition();
|
||||
assert_eq!(
|
||||
type_info.definitions.len(),
|
||||
type_info_second.definitions.len()
|
||||
);
|
||||
// let schema: JsonSchemaBevyType = type_info_second.into();
|
||||
// eprintln!("{}", serde_json::to_string_pretty(&schema).expect(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reflect_struct_with_hashmap() {
|
||||
#[derive(Reflect, Default, Deserialize, Serialize)]
|
||||
|
Loading…
Reference in New Issue
Block a user