ReflectJsonSchema for custom schemas

This commit is contained in:
Piotr Siuszko 2025-06-20 14:53:53 +02:00
parent 8e4809f123
commit 77c9da49d6
4 changed files with 102 additions and 9 deletions

View File

@ -518,6 +518,8 @@ impl Default for RemotePlugin {
impl Plugin for RemotePlugin { impl Plugin for RemotePlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.register_type_data::<schemas::open_rpc::OpenRpcDocument, schemas::ReflectJsonSchema>();
let mut remote_methods = RemoteMethods::new(); let mut remote_methods = RemoteMethods::new();
let plugin_methods = &mut *self.methods.write().unwrap(); let plugin_methods = &mut *self.methods.write().unwrap();

View File

@ -11,7 +11,7 @@ use serde_json::Value;
use crate::schemas::{ use crate::schemas::{
reflect_info::{SchemaInfoReflect, SchemaNumber}, reflect_info::{SchemaInfoReflect, SchemaNumber},
SchemaTypesMetadata, ReflectJsonSchema, SchemaTypesMetadata,
}; };
/// Helper trait for converting `TypeRegistration` to `JsonSchemaBevyType` /// Helper trait for converting `TypeRegistration` to `JsonSchemaBevyType`
@ -68,6 +68,9 @@ impl TypeRegistrySchemaReader for TypeRegistry {
impl From<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType { impl From<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType {
fn from(value: (&TypeRegistration, &SchemaTypesMetadata)) -> Self { fn from(value: (&TypeRegistration, &SchemaTypesMetadata)) -> Self {
let (reg, metadata) = value; let (reg, metadata) = value;
if let Some(s) = reg.data::<ReflectJsonSchema>() {
return s.0.clone();
}
let type_info = reg.type_info(); let type_info = reg.type_info();
let base_schema = type_info.build_schema(); let base_schema = type_info.build_schema();
@ -83,9 +86,14 @@ impl From<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType {
/// It tries to follow this standard: <https://json-schema.org/specification> /// It tries to follow this standard: <https://json-schema.org/specification>
/// ///
/// To take the full advantage from info provided by Bevy registry it provides extra fields /// To take the full advantage from info provided by Bevy registry it provides extra fields
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default, Reflect)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct JsonSchemaBevyType { pub struct JsonSchemaBevyType {
/// JSON Schema specific field.
/// This keyword is used to reference a statically identified schema.
#[serde(rename = "$ref")]
#[serde(skip_serializing_if = "Option::is_none", default)]
pub ref_type: Option<String>,
/// Bevy specific field, short path of the type. /// Bevy specific field, short path of the type.
#[serde(skip_serializing_if = "String::is_empty", default)] #[serde(skip_serializing_if = "String::is_empty", default)]
pub short_path: String, pub short_path: String,
@ -193,6 +201,7 @@ pub struct JsonSchemaBevyType {
pub description: Option<String>, pub description: Option<String>,
/// Default value for the schema. /// Default value for the schema.
#[serde(skip_serializing_if = "Option::is_none", default, rename = "default")] #[serde(skip_serializing_if = "Option::is_none", default, rename = "default")]
#[reflect(ignore)]
pub default_value: Option<Value>, pub default_value: Option<Value>,
} }
@ -251,7 +260,7 @@ impl JsonSchemaVariant {
} }
/// Kind of json schema, maps [`TypeInfo`] type /// Kind of json schema, maps [`TypeInfo`] type
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default, Reflect)]
pub enum SchemaKind { pub enum SchemaKind {
/// Struct /// Struct
#[default] #[default]
@ -528,6 +537,48 @@ mod tests {
assert!(schema.one_of.len() == 3, "Should have 3 possible schemas"); assert!(schema.one_of.len() == 3, "Should have 3 possible schemas");
} }
#[test]
fn reflect_export_with_custom_schema() {
#[derive(Reflect, Component)]
struct SomeType;
impl bevy_reflect::FromType<SomeType> for ReflectJsonSchema {
fn from_type() -> Self {
JsonSchemaBevyType {
ref_type: Some(
"https://raw.githubusercontent.com/open-rpc/meta-schema/master/schema.json"
.into(),
),
description: Some("Custom type for testing purposes.".to_string()),
..Default::default()
}
.into()
}
}
let atr = AppTypeRegistry::default();
{
let mut register = atr.write();
register.register::<SomeType>();
register.register_type_data::<SomeType, ReflectJsonSchema>();
}
let type_registry = atr.read();
let schema = type_registry
.export_type_json_schema::<SomeType>(&SchemaTypesMetadata::default())
.expect("Failed to export");
assert!(
!schema.reflect_types.contains(&"Component".to_owned()),
"Should not be a component"
);
assert!(
schema.ref_type.is_some_and(|t| !t.is_empty()),
"Should have a reference type"
);
assert!(
schema.description.is_some_and(|t| !t.is_empty()),
"Should have a description"
);
}
#[test] #[test]
fn reflect_export_tuple_struct() { fn reflect_export_tuple_struct() {
#[derive(Reflect, Component, Default, Deserialize, Serialize)] #[derive(Reflect, Component, Default, Deserialize, Serialize)]

View File

@ -1,4 +1,5 @@
//! Module with schemas used for various BRP endpoints //! Module with schemas used for various BRP endpoints
use bevy_derive::Deref;
use bevy_ecs::{ use bevy_ecs::{
reflect::{ReflectComponent, ReflectResource}, reflect::{ReflectComponent, ReflectResource},
resource::Resource, resource::Resource,
@ -10,6 +11,8 @@ use bevy_reflect::{
}; };
use core::any::TypeId; use core::any::TypeId;
use crate::schemas::json_schema::JsonSchemaBevyType;
pub mod json_schema; pub mod json_schema;
pub mod open_rpc; pub mod open_rpc;
pub mod reflect_info; pub mod reflect_info;
@ -23,6 +26,22 @@ pub struct SchemaTypesMetadata {
pub type_data_map: HashMap<TypeId, String>, pub type_data_map: HashMap<TypeId, String>,
} }
/// Reflect-compatible custom JSON Schema for this type
#[derive(Clone, Deref)]
pub struct ReflectJsonSchema(pub JsonSchemaBevyType);
impl From<&JsonSchemaBevyType> for ReflectJsonSchema {
fn from(schema: &JsonSchemaBevyType) -> Self {
Self(schema.clone())
}
}
impl From<JsonSchemaBevyType> for ReflectJsonSchema {
fn from(schema: JsonSchemaBevyType) -> Self {
Self(schema)
}
}
impl Default for SchemaTypesMetadata { impl Default for SchemaTypesMetadata {
fn default() -> Self { fn default() -> Self {
let mut data_types = Self { let mut data_types = Self {

View File

@ -1,15 +1,16 @@
//! Module with trimmed down `OpenRPC` document structs. //! Module with trimmed down `OpenRPC` document structs.
//! It tries to follow this standard: <https://spec.open-rpc.org> //! It tries to follow this standard: <https://spec.open-rpc.org>
use bevy_platform::collections::HashMap; use bevy_platform::collections::HashMap;
use bevy_reflect::{FromType, Reflect};
use bevy_utils::default; use bevy_utils::default;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::RemoteMethods; use crate::{schemas::ReflectJsonSchema, RemoteMethods};
use super::json_schema::JsonSchemaBevyType; use super::json_schema::JsonSchemaBevyType;
/// Represents an `OpenRPC` document as defined by the `OpenRPC` specification. /// Represents an `OpenRPC` document as defined by the `OpenRPC` specification.
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Reflect)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct OpenRpcDocument { pub struct OpenRpcDocument {
/// The version of the `OpenRPC` specification being used. /// The version of the `OpenRPC` specification being used.
@ -22,8 +23,24 @@ pub struct OpenRpcDocument {
pub servers: Option<Vec<ServerObject>>, pub servers: Option<Vec<ServerObject>>,
} }
impl FromType<OpenRpcDocument> for ReflectJsonSchema {
fn from_type() -> Self {
JsonSchemaBevyType {
ref_type: Some(
"https://raw.githubusercontent.com/open-rpc/meta-schema/master/schema.json".into(),
),
description: Some(
"Represents an `OpenRPC` document as defined by the `OpenRPC` specification."
.to_string(),
),
..default()
}
.into()
}
}
/// Contains metadata information about the `OpenRPC` document. /// Contains metadata information about the `OpenRPC` document.
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, Reflect)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct InfoObject { pub struct InfoObject {
/// The title of the API or document. /// The title of the API or document.
@ -35,6 +52,7 @@ pub struct InfoObject {
pub description: Option<String>, pub description: Option<String>,
/// A collection of custom extension fields. /// A collection of custom extension fields.
#[serde(flatten)] #[serde(flatten)]
#[reflect(ignore)]
pub extensions: HashMap<String, serde_json::Value>, pub extensions: HashMap<String, serde_json::Value>,
} }
@ -50,7 +68,7 @@ impl Default for InfoObject {
} }
/// Describes a server hosting the API as specified in the `OpenRPC` document. /// Describes a server hosting the API as specified in the `OpenRPC` document.
#[derive(Serialize, Deserialize, Debug, Default)] #[derive(Serialize, Deserialize, Debug, Default, Reflect)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ServerObject { pub struct ServerObject {
/// The name of the server. /// The name of the server.
@ -62,11 +80,12 @@ pub struct ServerObject {
pub description: Option<String>, pub description: Option<String>,
/// Additional custom extension fields. /// Additional custom extension fields.
#[serde(flatten)] #[serde(flatten)]
#[reflect(ignore)]
pub extensions: HashMap<String, serde_json::Value>, pub extensions: HashMap<String, serde_json::Value>,
} }
/// Represents an RPC method in the `OpenRPC` document. /// Represents an RPC method in the `OpenRPC` document.
#[derive(Serialize, Deserialize, Debug, Default)] #[derive(Serialize, Deserialize, Debug, Default, Reflect)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct MethodObject { pub struct MethodObject {
/// The method name (e.g., "/bevy/get") /// The method name (e.g., "/bevy/get")
@ -85,11 +104,12 @@ pub struct MethodObject {
// pub result: Option<Parameter>, // pub result: Option<Parameter>,
/// Additional custom extension fields. /// Additional custom extension fields.
#[serde(flatten)] #[serde(flatten)]
#[reflect(ignore)]
pub extensions: HashMap<String, serde_json::Value>, pub extensions: HashMap<String, serde_json::Value>,
} }
/// Represents an RPC method parameter in the `OpenRPC` document. /// Represents an RPC method parameter in the `OpenRPC` document.
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, Reflect)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Parameter { pub struct Parameter {
/// Parameter name /// Parameter name
@ -101,6 +121,7 @@ pub struct Parameter {
pub schema: JsonSchemaBevyType, pub schema: JsonSchemaBevyType,
/// Additional custom extension fields. /// Additional custom extension fields.
#[serde(flatten)] #[serde(flatten)]
#[reflect(ignore)]
pub extensions: HashMap<String, serde_json::Value>, pub extensions: HashMap<String, serde_json::Value>,
} }