Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_remote/src/schemas/json_schema.rs
6849 views
1
//! Module with JSON Schema type for Bevy Registry Types.
2
//! It tries to follow this standard: <https://json-schema.org/specification>
3
use alloc::borrow::Cow;
4
use bevy_platform::collections::HashMap;
5
use bevy_reflect::{
6
GetTypeRegistration, NamedField, OpaqueInfo, TypeInfo, TypeRegistration, TypeRegistry,
7
VariantInfo,
8
};
9
use core::any::TypeId;
10
use serde::{Deserialize, Serialize};
11
use serde_json::{json, Map, Value};
12
13
use crate::schemas::SchemaTypesMetadata;
14
15
/// Helper trait for converting `TypeRegistration` to `JsonSchemaBevyType`
16
pub trait TypeRegistrySchemaReader {
17
/// Export type JSON Schema.
18
fn export_type_json_schema<T: GetTypeRegistration + 'static>(
19
&self,
20
extra_info: &SchemaTypesMetadata,
21
) -> Option<JsonSchemaBevyType> {
22
self.export_type_json_schema_for_id(extra_info, TypeId::of::<T>())
23
}
24
/// Export type JSON Schema.
25
fn export_type_json_schema_for_id(
26
&self,
27
extra_info: &SchemaTypesMetadata,
28
type_id: TypeId,
29
) -> Option<JsonSchemaBevyType>;
30
}
31
32
impl TypeRegistrySchemaReader for TypeRegistry {
33
fn export_type_json_schema_for_id(
34
&self,
35
extra_info: &SchemaTypesMetadata,
36
type_id: TypeId,
37
) -> Option<JsonSchemaBevyType> {
38
let type_reg = self.get(type_id)?;
39
Some((type_reg, extra_info).into())
40
}
41
}
42
43
/// Exports schema info for a given type
44
pub fn export_type(
45
reg: &TypeRegistration,
46
metadata: &SchemaTypesMetadata,
47
) -> (Cow<'static, str>, JsonSchemaBevyType) {
48
(reg.type_info().type_path().into(), (reg, metadata).into())
49
}
50
51
impl From<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType {
52
fn from(value: (&TypeRegistration, &SchemaTypesMetadata)) -> Self {
53
let (reg, metadata) = value;
54
let t = reg.type_info();
55
let binding = t.type_path_table();
56
57
let short_path = binding.short_path();
58
let type_path = binding.path();
59
let mut typed_schema = JsonSchemaBevyType {
60
reflect_types: metadata.get_registered_reflect_types(reg),
61
short_path: short_path.to_owned(),
62
type_path: type_path.to_owned(),
63
crate_name: binding.crate_name().map(str::to_owned),
64
module_path: binding.module_path().map(str::to_owned),
65
..Default::default()
66
};
67
match t {
68
TypeInfo::Struct(info) => {
69
typed_schema.properties = info
70
.iter()
71
.map(|field| (field.name().to_owned(), field.ty().ref_type()))
72
.collect::<HashMap<_, _>>();
73
typed_schema.required = info
74
.iter()
75
.filter(|field| !field.type_path().starts_with("core::option::Option"))
76
.map(|f| f.name().to_owned())
77
.collect::<Vec<_>>();
78
typed_schema.additional_properties = Some(false);
79
typed_schema.schema_type = SchemaType::Object;
80
typed_schema.kind = SchemaKind::Struct;
81
}
82
TypeInfo::Enum(info) => {
83
typed_schema.kind = SchemaKind::Enum;
84
85
let simple = info
86
.iter()
87
.all(|variant| matches!(variant, VariantInfo::Unit(_)));
88
if simple {
89
typed_schema.schema_type = SchemaType::String;
90
typed_schema.one_of = info
91
.iter()
92
.map(|variant| match variant {
93
VariantInfo::Unit(v) => v.name().into(),
94
_ => unreachable!(),
95
})
96
.collect::<Vec<_>>();
97
} else {
98
typed_schema.schema_type = SchemaType::Object;
99
typed_schema.one_of = info
100
.iter()
101
.map(|variant| match variant {
102
VariantInfo::Struct(v) => json!({
103
"type": "object",
104
"kind": "Struct",
105
"typePath": format!("{}::{}", type_path, v.name()),
106
"shortPath": v.name(),
107
"properties": v
108
.iter()
109
.map(|field| (field.name().to_owned(), field.ref_type()))
110
.collect::<Map<_, _>>(),
111
"additionalProperties": false,
112
"required": v
113
.iter()
114
.filter(|field| !field.type_path().starts_with("core::option::Option"))
115
.map(NamedField::name)
116
.collect::<Vec<_>>(),
117
}),
118
VariantInfo::Tuple(v) => json!({
119
"type": "array",
120
"kind": "Tuple",
121
"typePath": format!("{}::{}", type_path, v.name()),
122
"shortPath": v.name(),
123
"prefixItems": v
124
.iter()
125
.map(SchemaJsonReference::ref_type)
126
.collect::<Vec<_>>(),
127
"items": false,
128
}),
129
VariantInfo::Unit(v) => json!({
130
"typePath": format!("{}::{}", type_path, v.name()),
131
"shortPath": v.name(),
132
}),
133
})
134
.collect::<Vec<_>>();
135
}
136
}
137
TypeInfo::TupleStruct(info) => {
138
typed_schema.schema_type = SchemaType::Array;
139
typed_schema.kind = SchemaKind::TupleStruct;
140
typed_schema.prefix_items = info
141
.iter()
142
.map(SchemaJsonReference::ref_type)
143
.collect::<Vec<_>>();
144
typed_schema.items = Some(false.into());
145
}
146
TypeInfo::List(info) => {
147
typed_schema.schema_type = SchemaType::Array;
148
typed_schema.kind = SchemaKind::List;
149
typed_schema.items = info.item_ty().ref_type().into();
150
}
151
TypeInfo::Array(info) => {
152
typed_schema.schema_type = SchemaType::Array;
153
typed_schema.kind = SchemaKind::Array;
154
typed_schema.items = info.item_ty().ref_type().into();
155
}
156
TypeInfo::Map(info) => {
157
typed_schema.schema_type = SchemaType::Object;
158
typed_schema.kind = SchemaKind::Map;
159
typed_schema.key_type = info.key_ty().ref_type().into();
160
typed_schema.value_type = info.value_ty().ref_type().into();
161
}
162
TypeInfo::Tuple(info) => {
163
typed_schema.schema_type = SchemaType::Array;
164
typed_schema.kind = SchemaKind::Tuple;
165
typed_schema.prefix_items = info
166
.iter()
167
.map(SchemaJsonReference::ref_type)
168
.collect::<Vec<_>>();
169
typed_schema.items = Some(false.into());
170
}
171
TypeInfo::Set(info) => {
172
typed_schema.schema_type = SchemaType::Set;
173
typed_schema.kind = SchemaKind::Set;
174
typed_schema.items = info.value_ty().ref_type().into();
175
}
176
TypeInfo::Opaque(info) => {
177
typed_schema.schema_type = info.map_json_type();
178
typed_schema.kind = SchemaKind::Value;
179
}
180
};
181
typed_schema
182
}
183
}
184
185
/// JSON Schema type for Bevy Registry Types
186
/// It tries to follow this standard: <https://json-schema.org/specification>
187
///
188
/// To take the full advantage from info provided by Bevy registry it provides extra fields
189
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
190
#[serde(rename_all = "camelCase")]
191
pub struct JsonSchemaBevyType {
192
/// Bevy specific field, short path of the type.
193
pub short_path: String,
194
/// Bevy specific field, full path of the type.
195
pub type_path: String,
196
/// Bevy specific field, path of the module that type is part of.
197
#[serde(skip_serializing_if = "Option::is_none", default)]
198
pub module_path: Option<String>,
199
/// Bevy specific field, name of the crate that type is part of.
200
#[serde(skip_serializing_if = "Option::is_none", default)]
201
pub crate_name: Option<String>,
202
/// Bevy specific field, names of the types that type reflects.
203
#[serde(skip_serializing_if = "Vec::is_empty", default)]
204
pub reflect_types: Vec<String>,
205
/// Bevy specific field, [`TypeInfo`] type mapping.
206
pub kind: SchemaKind,
207
/// Bevy specific field, provided when [`SchemaKind`] `kind` field is equal to [`SchemaKind::Map`].
208
///
209
/// It contains type info of key of the Map.
210
#[serde(skip_serializing_if = "Option::is_none", default)]
211
pub key_type: Option<Value>,
212
/// Bevy specific field, provided when [`SchemaKind`] `kind` field is equal to [`SchemaKind::Map`].
213
///
214
/// It contains type info of value of the Map.
215
#[serde(skip_serializing_if = "Option::is_none", default)]
216
pub value_type: Option<Value>,
217
/// The type keyword is fundamental to JSON Schema. It specifies the data type for a schema.
218
#[serde(rename = "type")]
219
pub schema_type: SchemaType,
220
/// The behavior of this keyword depends on the presence and annotation results of "properties"
221
/// and "patternProperties" within the same schema object.
222
/// Validation with "additionalProperties" applies only to the child
223
/// values of instance names that do not appear in the annotation results of either "properties" or "patternProperties".
224
#[serde(skip_serializing_if = "Option::is_none", default)]
225
pub additional_properties: Option<bool>,
226
/// Validation succeeds if, for each name that appears in both the instance and as a name
227
/// within this keyword's value, the child instance for that name successfully validates
228
/// against the corresponding schema.
229
#[serde(skip_serializing_if = "HashMap::is_empty", default)]
230
pub properties: HashMap<String, Value>,
231
/// An object instance is valid against this keyword if every item in the array is the name of a property in the instance.
232
#[serde(skip_serializing_if = "Vec::is_empty", default)]
233
pub required: Vec<String>,
234
/// An instance validates successfully against this keyword if it validates successfully against exactly one schema defined by this keyword's value.
235
#[serde(skip_serializing_if = "Vec::is_empty", default)]
236
pub one_of: Vec<Value>,
237
/// 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.
238
///
239
/// This keyword produces an annotation value which is the largest index to which this keyword
240
/// applied a subschema. The value MAY be a boolean true if a subschema was applied to every
241
/// index of the instance, such as is produced by the "items" keyword.
242
/// This annotation affects the behavior of "items" and "unevaluatedItems".
243
#[serde(skip_serializing_if = "Vec::is_empty", default)]
244
pub prefix_items: Vec<Value>,
245
/// This keyword applies its subschema to all instance elements at indexes greater
246
/// than the length of the "prefixItems" array in the same schema object,
247
/// as reported by the annotation result of that "prefixItems" keyword.
248
/// If no such annotation result exists, "items" applies its subschema to all
249
/// instance array elements.
250
///
251
/// If the "items" subschema is applied to any positions within the instance array,
252
/// it produces an annotation result of boolean true, indicating that all remaining
253
/// array elements have been evaluated against this keyword's subschema.
254
#[serde(skip_serializing_if = "Option::is_none", default)]
255
pub items: Option<Value>,
256
}
257
258
/// Kind of json schema, maps [`TypeInfo`] type
259
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
260
pub enum SchemaKind {
261
/// Struct
262
#[default]
263
Struct,
264
/// Enum type
265
Enum,
266
/// A key-value map
267
Map,
268
/// Array
269
Array,
270
/// List
271
List,
272
/// Fixed size collection of items
273
Tuple,
274
/// Fixed size collection of items with named fields
275
TupleStruct,
276
/// Set of unique values
277
Set,
278
/// Single value, eg. primitive types
279
Value,
280
}
281
282
/// Type of json schema
283
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
284
#[serde(rename_all = "lowercase")]
285
pub enum SchemaType {
286
/// Represents a string value.
287
String,
288
289
/// Represents a floating-point number.
290
Float,
291
292
/// Represents an unsigned integer.
293
Uint,
294
295
/// Represents a signed integer.
296
Int,
297
298
/// Represents an object with key-value pairs.
299
Object,
300
301
/// Represents an array of values.
302
Array,
303
304
/// Represents a boolean value (true or false).
305
Boolean,
306
307
/// Represents a set of unique values.
308
Set,
309
310
/// Represents a null value.
311
#[default]
312
Null,
313
}
314
315
/// Helper trait for generating json schema reference
316
trait SchemaJsonReference {
317
/// Reference to another type in schema.
318
/// The value `$ref` is a URI-reference that is resolved against the schema.
319
fn ref_type(self) -> Value;
320
}
321
322
/// Helper trait for mapping bevy type path into json schema type
323
pub trait SchemaJsonType {
324
/// Bevy Reflect type path
325
fn get_type_path(&self) -> &'static str;
326
327
/// JSON Schema type keyword from Bevy reflect type path into
328
fn map_json_type(&self) -> SchemaType {
329
match self.get_type_path() {
330
"bool" => SchemaType::Boolean,
331
"u8" | "u16" | "u32" | "u64" | "u128" | "usize" => SchemaType::Uint,
332
"i8" | "i16" | "i32" | "i64" | "i128" | "isize" => SchemaType::Int,
333
"f32" | "f64" => SchemaType::Float,
334
"char" | "str" | "alloc::string::String" => SchemaType::String,
335
_ => SchemaType::Object,
336
}
337
}
338
}
339
340
impl SchemaJsonType for OpaqueInfo {
341
fn get_type_path(&self) -> &'static str {
342
self.type_path()
343
}
344
}
345
346
impl SchemaJsonReference for &bevy_reflect::Type {
347
fn ref_type(self) -> Value {
348
let path = self.path();
349
json!({"type": json!({ "$ref": format!("#/$defs/{path}") })})
350
}
351
}
352
353
impl SchemaJsonReference for &bevy_reflect::UnnamedField {
354
fn ref_type(self) -> Value {
355
let path = self.type_path();
356
json!({"type": json!({ "$ref": format!("#/$defs/{path}") })})
357
}
358
}
359
360
impl SchemaJsonReference for &NamedField {
361
fn ref_type(self) -> Value {
362
let type_path = self.type_path();
363
json!({"type": json!({ "$ref": format!("#/$defs/{type_path}") }), "typePath": self.name()})
364
}
365
}
366
367
#[cfg(test)]
368
mod tests {
369
use super::*;
370
use bevy_ecs::prelude::ReflectComponent;
371
use bevy_ecs::prelude::ReflectResource;
372
373
use bevy_ecs::{component::Component, reflect::AppTypeRegistry, resource::Resource};
374
use bevy_reflect::prelude::ReflectDefault;
375
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
376
377
#[test]
378
fn reflect_export_struct() {
379
#[derive(Reflect, Resource, Default, Deserialize, Serialize)]
380
#[reflect(Resource, Default, Serialize, Deserialize)]
381
struct Foo {
382
a: f32,
383
b: Option<f32>,
384
}
385
386
let atr = AppTypeRegistry::default();
387
{
388
let mut register = atr.write();
389
register.register::<Foo>();
390
}
391
let type_registry = atr.read();
392
let foo_registration = type_registry
393
.get(TypeId::of::<Foo>())
394
.expect("SHOULD BE REGISTERED")
395
.clone();
396
let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());
397
398
assert!(
399
!schema.reflect_types.contains(&"Component".to_owned()),
400
"Should not be a component"
401
);
402
assert!(
403
schema.reflect_types.contains(&"Resource".to_owned()),
404
"Should be a resource"
405
);
406
let _ = schema.properties.get("a").expect("Missing `a` field");
407
let _ = schema.properties.get("b").expect("Missing `b` field");
408
assert!(
409
schema.required.contains(&"a".to_owned()),
410
"Field a should be required"
411
);
412
assert!(
413
!schema.required.contains(&"b".to_owned()),
414
"Field b should not be required"
415
);
416
}
417
418
#[test]
419
fn reflect_export_enum() {
420
#[derive(Reflect, Component, Default, Deserialize, Serialize)]
421
#[reflect(Component, Default, Serialize, Deserialize)]
422
enum EnumComponent {
423
ValueOne(i32),
424
ValueTwo {
425
test: i32,
426
},
427
#[default]
428
NoValue,
429
}
430
431
let atr = AppTypeRegistry::default();
432
{
433
let mut register = atr.write();
434
register.register::<EnumComponent>();
435
}
436
let type_registry = atr.read();
437
let foo_registration = type_registry
438
.get(TypeId::of::<EnumComponent>())
439
.expect("SHOULD BE REGISTERED")
440
.clone();
441
let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());
442
assert!(
443
schema.reflect_types.contains(&"Component".to_owned()),
444
"Should be a component"
445
);
446
assert!(
447
!schema.reflect_types.contains(&"Resource".to_owned()),
448
"Should not be a resource"
449
);
450
assert!(schema.properties.is_empty(), "Should not have any field");
451
assert!(schema.one_of.len() == 3, "Should have 3 possible schemas");
452
}
453
454
#[test]
455
fn reflect_export_struct_without_reflect_types() {
456
#[derive(Reflect, Component, Default, Deserialize, Serialize)]
457
enum EnumComponent {
458
ValueOne(i32),
459
ValueTwo {
460
test: i32,
461
},
462
#[default]
463
NoValue,
464
}
465
466
let atr = AppTypeRegistry::default();
467
{
468
let mut register = atr.write();
469
register.register::<EnumComponent>();
470
}
471
let type_registry = atr.read();
472
let foo_registration = type_registry
473
.get(TypeId::of::<EnumComponent>())
474
.expect("SHOULD BE REGISTERED")
475
.clone();
476
let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());
477
assert!(
478
!schema.reflect_types.contains(&"Component".to_owned()),
479
"Should not be a component"
480
);
481
assert!(
482
!schema.reflect_types.contains(&"Resource".to_owned()),
483
"Should not be a resource"
484
);
485
assert!(schema.properties.is_empty(), "Should not have any field");
486
assert!(schema.one_of.len() == 3, "Should have 3 possible schemas");
487
}
488
489
#[test]
490
fn reflect_struct_with_custom_type_data() {
491
#[derive(Reflect, Default, Deserialize, Serialize)]
492
#[reflect(Default)]
493
enum EnumComponent {
494
ValueOne(i32),
495
ValueTwo {
496
test: i32,
497
},
498
#[default]
499
NoValue,
500
}
501
502
#[derive(Clone)]
503
pub struct ReflectCustomData;
504
505
impl<T: Reflect> bevy_reflect::FromType<T> for ReflectCustomData {
506
fn from_type() -> Self {
507
ReflectCustomData
508
}
509
}
510
511
let atr = AppTypeRegistry::default();
512
{
513
let mut register = atr.write();
514
register.register::<EnumComponent>();
515
register.register_type_data::<EnumComponent, ReflectCustomData>();
516
}
517
let mut metadata = SchemaTypesMetadata::default();
518
metadata.map_type_data::<ReflectCustomData>("CustomData");
519
let type_registry = atr.read();
520
let foo_registration = type_registry
521
.get(TypeId::of::<EnumComponent>())
522
.expect("SHOULD BE REGISTERED")
523
.clone();
524
let (_, schema) = export_type(&foo_registration, &metadata);
525
assert!(
526
!metadata.has_type_data::<ReflectComponent>(&schema.reflect_types),
527
"Should not be a component"
528
);
529
assert!(
530
!metadata.has_type_data::<ReflectResource>(&schema.reflect_types),
531
"Should not be a resource"
532
);
533
assert!(
534
metadata.has_type_data::<ReflectDefault>(&schema.reflect_types),
535
"Should have default"
536
);
537
assert!(
538
metadata.has_type_data::<ReflectCustomData>(&schema.reflect_types),
539
"Should have CustomData"
540
);
541
assert!(schema.properties.is_empty(), "Should not have any field");
542
assert!(schema.one_of.len() == 3, "Should have 3 possible schemas");
543
}
544
545
#[test]
546
fn reflect_export_tuple_struct() {
547
#[derive(Reflect, Component, Default, Deserialize, Serialize)]
548
#[reflect(Component, Default, Serialize, Deserialize)]
549
struct TupleStructType(usize, i32);
550
551
let atr = AppTypeRegistry::default();
552
{
553
let mut register = atr.write();
554
register.register::<TupleStructType>();
555
}
556
let type_registry = atr.read();
557
let foo_registration = type_registry
558
.get(TypeId::of::<TupleStructType>())
559
.expect("SHOULD BE REGISTERED")
560
.clone();
561
let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());
562
assert!(
563
schema.reflect_types.contains(&"Component".to_owned()),
564
"Should be a component"
565
);
566
assert!(
567
!schema.reflect_types.contains(&"Resource".to_owned()),
568
"Should not be a resource"
569
);
570
assert!(schema.properties.is_empty(), "Should not have any field");
571
assert!(schema.prefix_items.len() == 2, "Should have 2 prefix items");
572
}
573
574
#[test]
575
fn reflect_export_serialization_check() {
576
#[derive(Reflect, Resource, Default, Deserialize, Serialize)]
577
#[reflect(Resource, Default)]
578
struct Foo {
579
a: f32,
580
}
581
582
let atr = AppTypeRegistry::default();
583
{
584
let mut register = atr.write();
585
register.register::<Foo>();
586
}
587
let type_registry = atr.read();
588
let foo_registration = type_registry
589
.get(TypeId::of::<Foo>())
590
.expect("SHOULD BE REGISTERED")
591
.clone();
592
let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default());
593
let schema_as_value = serde_json::to_value(&schema).expect("Should serialize");
594
let value = json!({
595
"shortPath": "Foo",
596
"typePath": "bevy_remote::schemas::json_schema::tests::Foo",
597
"modulePath": "bevy_remote::schemas::json_schema::tests",
598
"crateName": "bevy_remote",
599
"reflectTypes": [
600
"Resource",
601
"Default",
602
],
603
"kind": "Struct",
604
"type": "object",
605
"additionalProperties": false,
606
"properties": {
607
"a": {
608
"type": {
609
"$ref": "#/$defs/f32"
610
}
611
},
612
},
613
"required": [
614
"a"
615
]
616
});
617
assert_normalized_values(schema_as_value, value);
618
}
619
620
/// This function exist to avoid false failures due to ordering differences between `serde_json` values.
621
fn assert_normalized_values(mut one: Value, mut two: Value) {
622
normalize_json(&mut one);
623
normalize_json(&mut two);
624
assert_eq!(one, two);
625
626
/// Recursively sorts arrays in a `serde_json::Value`
627
fn normalize_json(value: &mut Value) {
628
match value {
629
Value::Array(arr) => {
630
for v in arr.iter_mut() {
631
normalize_json(v);
632
}
633
arr.sort_by_key(ToString::to_string); // Sort by stringified version
634
}
635
Value::Object(map) => {
636
for (_k, v) in map.iter_mut() {
637
normalize_json(v);
638
}
639
}
640
_ => {}
641
}
642
}
643
}
644
}
645
646