Introduce fields: title, rust_fields_info, replace pattern_properties
with pattern and property_names
This commit is contained in:
parent
8c5604e88a
commit
f404446756
@ -101,9 +101,21 @@ pub struct JsonSchemaBevyType {
|
||||
pub schema: Option<Cow<'static, str>>,
|
||||
/// JSON Schema specific field.
|
||||
/// This keyword is used to reference a statically identified schema.
|
||||
///
|
||||
/// Serialization format matches RFC 3986, which means that the reference must be a valid URI.
|
||||
/// During serialization, all the reserved characters are encoded as percent-encoded sequences.
|
||||
#[serde(rename = "$ref")]
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub ref_type: Option<TypeReferencePath>,
|
||||
/// JSON Schema specific field.
|
||||
///
|
||||
/// The title keyword is a placeholder for a concise human-readable string
|
||||
/// summary of what a schema or any of its subschemas are about.
|
||||
///
|
||||
/// Bevy uses this field to provide a field name for the schema.
|
||||
/// It can contain dots to indicate nested fields.
|
||||
#[serde(skip_serializing_if = "str::is_empty", default)]
|
||||
pub title: Cow<'static, str>,
|
||||
/// Bevy specific field, short path of the type.
|
||||
#[serde(skip_serializing_if = "str::is_empty", default)]
|
||||
pub short_path: Cow<'static, str>,
|
||||
@ -140,6 +152,13 @@ pub struct JsonSchemaBevyType {
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
#[reflect(ignore)]
|
||||
pub key_type: Option<Box<JsonSchemaBevyType>>,
|
||||
/// Bevy specific field.
|
||||
///
|
||||
/// It is provided when type is serialized as array, but the type is not an array.
|
||||
/// It is done to provide additional information about the fields in the schema.
|
||||
#[serde(skip_serializing_if = "HashMap::is_empty", default)]
|
||||
#[reflect(ignore)]
|
||||
pub rust_fields_info: HashMap<Cow<'static, str>, Box<JsonSchemaBevyType>>,
|
||||
/// 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")]
|
||||
@ -150,13 +169,14 @@ pub struct JsonSchemaBevyType {
|
||||
/// 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<JsonSchemaVariant>,
|
||||
/// The behavior of this keyword depends on the presence and annotation results of "properties"
|
||||
/// and "patternProperties" within the same schema object.
|
||||
/// 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 = "HashMap::is_empty", default)]
|
||||
/// This keyword restricts object instances to only define properties whose names match the given schema.
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
#[reflect(ignore)]
|
||||
pub pattern_properties: HashMap<Cow<'static, str>, Box<JsonSchemaBevyType>>,
|
||||
pub property_names: Option<Box<JsonSchemaBevyType>>,
|
||||
/// The pattern keyword restricts string instances to match the given regular expression.
|
||||
/// For now used mostly when limiting property names for Map types.
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub pattern: Option<Cow<'static, str>>,
|
||||
/// 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.
|
||||
@ -732,11 +752,13 @@ mod tests {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"a": {
|
||||
"title": "a",
|
||||
"type": "number",
|
||||
"kind": "Value",
|
||||
"typePath": "f32"
|
||||
},
|
||||
"b": {
|
||||
"title": "b",
|
||||
"minimum": 0,
|
||||
"maximum": 255,
|
||||
"type": "integer",
|
||||
|
@ -43,7 +43,7 @@ impl CustomInternalSchemaData {
|
||||
bevy_reflect::TypeInfo::Struct(struct_info) => Some(CustomInternalSchemaData(
|
||||
InternalSchemaType::FieldsHolder(FieldsInformation::new(
|
||||
struct_info.iter(),
|
||||
reflect_info::FieldType::ForceUnnamed,
|
||||
reflect_info::FieldType::ForceUnnamed(struct_info.ty().id()),
|
||||
)),
|
||||
)),
|
||||
_ => None,
|
||||
@ -69,6 +69,7 @@ pub(crate) trait RegisterReflectJsonSchemas {
|
||||
self.register_force_as_array::<bevy_math::U64Vec2>();
|
||||
self.register_force_as_array::<bevy_math::BVec2>();
|
||||
|
||||
self.register_force_as_array::<bevy_math::Vec3A>();
|
||||
self.register_force_as_array::<bevy_math::Vec3>();
|
||||
self.register_force_as_array::<bevy_math::DVec3>();
|
||||
self.register_force_as_array::<bevy_math::I8Vec3>();
|
||||
@ -95,6 +96,18 @@ pub(crate) trait RegisterReflectJsonSchemas {
|
||||
|
||||
self.register_force_as_array::<bevy_math::Quat>();
|
||||
self.register_force_as_array::<bevy_math::DQuat>();
|
||||
|
||||
self.register_force_as_array::<bevy_math::Mat2>();
|
||||
self.register_force_as_array::<bevy_math::DMat2>();
|
||||
self.register_force_as_array::<bevy_math::DMat3>();
|
||||
self.register_force_as_array::<bevy_math::Mat3A>();
|
||||
self.register_force_as_array::<bevy_math::Mat3>();
|
||||
self.register_force_as_array::<bevy_math::DMat4>();
|
||||
self.register_force_as_array::<bevy_math::Mat4>();
|
||||
self.register_force_as_array::<bevy_math::Affine2>();
|
||||
self.register_force_as_array::<bevy_math::DAffine2>();
|
||||
self.register_force_as_array::<bevy_math::DAffine3>();
|
||||
self.register_force_as_array::<bevy_math::Affine3A>();
|
||||
}
|
||||
self.register_type_internal::<OpenRpcDocument>();
|
||||
self.register_type_data_internal::<OpenRpcDocument, ReflectJsonSchema>();
|
||||
@ -147,9 +160,9 @@ impl RegisterReflectJsonSchemas for bevy_reflect::TypeRegistry {
|
||||
T: Reflect + TypePath + GetTypeRegistration,
|
||||
D: TypeData,
|
||||
{
|
||||
self.get_mut(TypeId::of::<T>())
|
||||
.expect("SHOULD NOT HAPPENED")
|
||||
.insert(data);
|
||||
if let Some(type_reg) = self.get_mut(TypeId::of::<T>()) {
|
||||
type_reg.insert(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
impl RegisterReflectJsonSchemas for bevy_app::App {
|
||||
|
@ -263,6 +263,8 @@ pub(super) static BASE_TYPES_INFO: LazyLock<HashMap<TypeId, PrimitiveTypeInfo>>
|
||||
Deserialize,
|
||||
)]
|
||||
/// Reference id of the type.
|
||||
/// Serialization format matches RFC 3986, which means that the reference must be a valid URI.
|
||||
/// During serialization, all the reserved characters are encoded as percent-encoded sequences.
|
||||
pub struct TypeReferenceId(Cow<'static, str>);
|
||||
|
||||
impl Display for TypeReferenceId {
|
||||
@ -273,7 +275,8 @@ impl Display for TypeReferenceId {
|
||||
|
||||
impl From<&str> for TypeReferenceId {
|
||||
fn from(t: &str) -> Self {
|
||||
TypeReferenceId(t.to_string().into())
|
||||
let data = decode_from_uri(t).unwrap_or(t.to_string());
|
||||
TypeReferenceId(data.into())
|
||||
}
|
||||
}
|
||||
|
||||
@ -290,7 +293,7 @@ impl From<&TypePathTable> for TypeReferenceId {
|
||||
}
|
||||
|
||||
/// Information about the field type.
|
||||
#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Hash, Default, Reflect)]
|
||||
#[derive(Clone, Debug, PartialEq, Default, Reflect)]
|
||||
pub enum FieldType {
|
||||
/// Named field type.
|
||||
Named,
|
||||
@ -298,7 +301,7 @@ pub enum FieldType {
|
||||
#[default]
|
||||
Unnamed,
|
||||
/// Named field type that is stored as unnamed. Example: glam Vec3.
|
||||
ForceUnnamed,
|
||||
ForceUnnamed(TypeId),
|
||||
}
|
||||
|
||||
/// Information about the attributes of a field.
|
||||
@ -322,6 +325,48 @@ impl FieldsInformation {
|
||||
fields_type,
|
||||
}
|
||||
}
|
||||
/// Creates a new instance of `FieldsInformation` with given fields.
|
||||
pub fn with_fields(fields: Vec<SchemaFieldData>, fields_type: FieldType) -> Self {
|
||||
FieldsInformation {
|
||||
fields,
|
||||
fields_type,
|
||||
}
|
||||
}
|
||||
fn try_get_fields_recursively(
|
||||
registry: &TypeRegistry,
|
||||
type_id: TypeId,
|
||||
field_prefix: &str,
|
||||
) -> Option<Vec<SchemaFieldData>> {
|
||||
let type_reg = registry.get(type_id)?;
|
||||
let internal = InternalSchemaType::from_type_registration(type_reg, registry);
|
||||
let InternalSchemaType::FieldsHolder(fields_info) = internal else {
|
||||
return None;
|
||||
};
|
||||
let mut fields = Vec::new();
|
||||
for field in fields_info.fields.iter() {
|
||||
let new_prefix = if field_prefix.is_empty() {
|
||||
field.to_name().to_string()
|
||||
} else {
|
||||
format!("{}.{}", field_prefix, field.to_name())
|
||||
};
|
||||
let extra_fields =
|
||||
Self::try_get_fields_recursively(registry, field.type_id, &new_prefix);
|
||||
if let Some(extra_fields) = extra_fields {
|
||||
for extra_field in extra_fields {
|
||||
fields.push(SchemaFieldData {
|
||||
name: Some(extra_field.to_name()),
|
||||
..extra_field
|
||||
});
|
||||
}
|
||||
} else {
|
||||
fields.push(SchemaFieldData {
|
||||
name: Some(new_prefix.into()),
|
||||
..field.clone()
|
||||
});
|
||||
}
|
||||
}
|
||||
Some(fields)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deref)]
|
||||
@ -390,7 +435,7 @@ fn try_get_regex_for_type(id: TypeId) -> Option<Cow<'static, str>> {
|
||||
}
|
||||
|
||||
/// Represents the data of a field in a schema.
|
||||
#[derive(Clone, Reflect, Debug)]
|
||||
#[derive(Clone, Reflect, Debug, PartialEq)]
|
||||
pub struct SchemaFieldData {
|
||||
/// Name of the field.
|
||||
pub name: Option<Cow<'static, str>>,
|
||||
@ -449,6 +494,28 @@ fn encode_to_uri(input: &str) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
fn decode_from_uri(encoded: &str) -> Option<String> {
|
||||
let bytes = encoded.as_bytes();
|
||||
let length = bytes.len();
|
||||
let mut decoded: Vec<u8> = Vec::with_capacity(length);
|
||||
let mut i = 0;
|
||||
while i < length - 2 {
|
||||
match bytes[i] {
|
||||
b'%' => {
|
||||
i += 3;
|
||||
}
|
||||
b => {
|
||||
decoded.push(b);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
decoded.push(bytes[length - 2]);
|
||||
decoded.push(bytes[length - 1]);
|
||||
|
||||
String::from_utf8(decoded).ok()
|
||||
}
|
||||
|
||||
impl TypeReferencePath {
|
||||
/// Creates a new `TypeReferencePath` with the given type path at the Definitions location.
|
||||
pub fn definition(id: impl Into<TypeReferenceId>) -> Self {
|
||||
@ -1277,6 +1344,8 @@ pub(crate) fn variant_to_definition(
|
||||
}
|
||||
|
||||
pub(crate) trait TypeDefinitionBuilder {
|
||||
/// Returns the type registry.
|
||||
fn get_type_registry(&self) -> &TypeRegistry;
|
||||
/// Builds a JSON schema for a given type ID.
|
||||
fn build_schema_for_type_id(
|
||||
&self,
|
||||
@ -1343,20 +1412,46 @@ pub(crate) trait TypeDefinitionBuilder {
|
||||
*schema = new_schema;
|
||||
schema.kind = Some(SchemaKind::Tuple);
|
||||
}
|
||||
FieldType::Unnamed | FieldType::ForceUnnamed => {
|
||||
FieldType::Unnamed => {
|
||||
schema.min_items = Some(info.fields.len() as u64);
|
||||
schema.max_items = Some(info.fields.len() as u64);
|
||||
schema.prefix_items = info
|
||||
.fields
|
||||
.iter()
|
||||
.map(|field| {
|
||||
let mut schema = self
|
||||
.build_schema_reference_for_type_id(field.type_id, Some(field))
|
||||
.unwrap_or_default();
|
||||
if schema.description.is_none() && field.name.is_some() {
|
||||
schema.description = field.name.clone();
|
||||
}
|
||||
JsonSchemaVariant::Schema(Box::new(schema))
|
||||
self.build_schema_reference_for_type_id(field.type_id, Some(field))
|
||||
.unwrap_or_default()
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
FieldType::ForceUnnamed(type_id) => {
|
||||
let fields = FieldsInformation::try_get_fields_recursively(
|
||||
self.get_type_registry(),
|
||||
*type_id,
|
||||
"",
|
||||
)
|
||||
.unwrap_or_default();
|
||||
schema.min_items = Some(fields.len() as u64);
|
||||
schema.max_items = Some(fields.len() as u64);
|
||||
schema.prefix_items = fields
|
||||
.iter()
|
||||
.map(|field| {
|
||||
self.build_schema_reference_for_type_id(field.type_id, Some(field))
|
||||
.unwrap_or_default()
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
schema.rust_fields_info = info
|
||||
.fields
|
||||
.iter()
|
||||
.map(|field| {
|
||||
(
|
||||
field.to_name(),
|
||||
self.build_schema_reference_for_type_id(field.type_id, Some(field))
|
||||
.unwrap_or_default()
|
||||
.into(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
@ -1365,6 +1460,9 @@ pub(crate) trait TypeDefinitionBuilder {
|
||||
}
|
||||
|
||||
impl TypeDefinitionBuilder for TypeRegistry {
|
||||
fn get_type_registry(&self) -> &TypeRegistry {
|
||||
self
|
||||
}
|
||||
fn build_schema_for_type_id(
|
||||
&self,
|
||||
type_id: TypeId,
|
||||
@ -1428,14 +1526,16 @@ impl TypeDefinitionBuilder for TypeRegistry {
|
||||
schema.value_type = self
|
||||
.build_schema_reference_for_type_id(value, None)
|
||||
.map(Box::new);
|
||||
if let Some(p) = try_get_regex_for_type(key) {
|
||||
schema.pattern_properties =
|
||||
[(p, schema.value_type.clone().unwrap_or_default())].into();
|
||||
schema.additional_properties = Some(JsonSchemaVariant::BoolValue(false));
|
||||
} else {
|
||||
schema.additional_properties =
|
||||
schema.value_type.clone().map(JsonSchemaVariant::Schema);
|
||||
}
|
||||
schema.property_names = Some(
|
||||
JsonSchemaBevyType {
|
||||
pattern: try_get_regex_for_type(key),
|
||||
schema_type: SchemaType::String.into(),
|
||||
..default()
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
schema.additional_properties =
|
||||
schema.value_type.clone().map(JsonSchemaVariant::Schema);
|
||||
schema.key_type = self
|
||||
.build_schema_reference_for_type_id(key, None)
|
||||
.map(Box::new);
|
||||
@ -1523,6 +1623,7 @@ impl TypeDefinitionBuilder for TypeRegistry {
|
||||
) -> Option<JsonSchemaBevyType> {
|
||||
let type_reg = self.get(type_id)?;
|
||||
let description = field_data.and_then(SchemaFieldData::to_description);
|
||||
let title = field_data.map(SchemaFieldData::to_name).unwrap_or_default();
|
||||
|
||||
let ref_type = Some(TypeReferencePath::definition(
|
||||
type_reg.type_info().type_path(),
|
||||
@ -1532,6 +1633,7 @@ impl TypeDefinitionBuilder for TypeRegistry {
|
||||
description,
|
||||
kind: Some(SchemaKind::from_type_reg(type_reg)),
|
||||
ref_type,
|
||||
title,
|
||||
type_path: type_reg.type_info().type_path().into(),
|
||||
schema_type: None,
|
||||
..default()
|
||||
@ -1591,15 +1693,16 @@ impl TypeDefinitionBuilder for TypeRegistry {
|
||||
schema.value_type = self
|
||||
.build_schema_reference_for_type_id(value, None)
|
||||
.map(Box::new);
|
||||
|
||||
if let Some(p) = try_get_regex_for_type(key) {
|
||||
schema.pattern_properties =
|
||||
[(p, schema.value_type.clone().unwrap_or_default())].into();
|
||||
schema.additional_properties = Some(JsonSchemaVariant::BoolValue(false));
|
||||
} else {
|
||||
schema.additional_properties =
|
||||
schema.value_type.clone().map(JsonSchemaVariant::Schema);
|
||||
}
|
||||
schema.additional_properties =
|
||||
schema.value_type.clone().map(JsonSchemaVariant::Schema);
|
||||
schema.property_names = Some(
|
||||
JsonSchemaBevyType {
|
||||
pattern: try_get_regex_for_type(key),
|
||||
schema_type: SchemaType::String.into(),
|
||||
..default()
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
InternalSchemaType::Optional { generic } => {
|
||||
let schema_optional = self
|
||||
@ -1772,8 +1875,9 @@ pub(super) mod tests {
|
||||
|
||||
let mut register = atr.write();
|
||||
register.register::<bevy_math::Vec3>();
|
||||
register.register::<bevy_math::DAffine3>();
|
||||
register.register::<Foo>();
|
||||
register.register_force_as_array::<bevy_math::Vec3>();
|
||||
register.register_schema_base_types();
|
||||
}
|
||||
let type_registry = atr.read();
|
||||
let (_, schema) = type_registry
|
||||
|
Loading…
Reference in New Issue
Block a user