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 {
fn build(&self, app: &mut App) {
app.register_type_data::<schemas::open_rpc::OpenRpcDocument, schemas::ReflectJsonSchema>();
let mut remote_methods = RemoteMethods::new();
let plugin_methods = &mut *self.methods.write().unwrap();

View File

@ -11,7 +11,7 @@ use serde_json::Value;
use crate::schemas::{
reflect_info::{SchemaInfoReflect, SchemaNumber},
SchemaTypesMetadata,
ReflectJsonSchema, SchemaTypesMetadata,
};
/// Helper trait for converting `TypeRegistration` to `JsonSchemaBevyType`
@ -68,6 +68,9 @@ impl TypeRegistrySchemaReader for TypeRegistry {
impl From<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType {
fn from(value: (&TypeRegistration, &SchemaTypesMetadata)) -> Self {
let (reg, metadata) = value;
if let Some(s) = reg.data::<ReflectJsonSchema>() {
return s.0.clone();
}
let type_info = reg.type_info();
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>
///
/// 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")]
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.
#[serde(skip_serializing_if = "String::is_empty", default)]
pub short_path: String,
@ -193,6 +201,7 @@ pub struct JsonSchemaBevyType {
pub description: Option<String>,
/// Default value for the schema.
#[serde(skip_serializing_if = "Option::is_none", default, rename = "default")]
#[reflect(ignore)]
pub default_value: Option<Value>,
}
@ -251,7 +260,7 @@ impl JsonSchemaVariant {
}
/// 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 {
/// Struct
#[default]
@ -528,6 +537,48 @@ mod tests {
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]
fn reflect_export_tuple_struct() {
#[derive(Reflect, Component, Default, Deserialize, Serialize)]

View File

@ -1,4 +1,5 @@
//! Module with schemas used for various BRP endpoints
use bevy_derive::Deref;
use bevy_ecs::{
reflect::{ReflectComponent, ReflectResource},
resource::Resource,
@ -10,6 +11,8 @@ use bevy_reflect::{
};
use core::any::TypeId;
use crate::schemas::json_schema::JsonSchemaBevyType;
pub mod json_schema;
pub mod open_rpc;
pub mod reflect_info;
@ -23,6 +26,22 @@ pub struct SchemaTypesMetadata {
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 {
fn default() -> Self {
let mut data_types = Self {

View File

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