1use serde_json::Value;
7
8pub trait GtsSchema {
25 const SCHEMA_ID: &'static str;
27
28 const GENERIC_FIELD: Option<&'static str> = None;
31
32 fn gts_schema_with_refs() -> Value;
34
35 #[must_use]
40 fn gts_schema() -> Value {
41 Self::gts_schema_with_refs()
42 }
43
44 #[must_use]
57 fn gts_schema_with_refs_allof() -> Value {
58 Self::gts_schema_with_refs()
59 }
60
61 #[must_use]
64 fn innermost_schema_id() -> &'static str {
65 Self::SCHEMA_ID
66 }
67
68 #[must_use]
71 fn innermost_schema() -> Value {
72 Self::gts_schema_with_refs()
73 }
74
75 #[must_use]
78 fn collect_nesting_path() -> Vec<&'static str> {
79 Vec::new()
80 }
81
82 #[must_use]
95 fn wrap_in_nesting_path(
96 path: &[&str],
97 properties: Value,
98 required: Value,
99 generic_field: Option<&str>,
100 ) -> Value {
101 if path.is_empty() {
102 return properties;
103 }
104
105 let mut current = serde_json::json!({
108 "type": "object",
109 "additionalProperties": false,
110 "properties": properties,
111 "required": required
112 });
113
114 if let Some(gf) = generic_field
117 && let Some(props) = current
118 .get_mut("properties")
119 .and_then(|v| v.as_object_mut())
120 && props.contains_key(gf)
121 {
122 props.insert(gf.to_owned(), serde_json::json!({"type": "object"}));
123 }
124
125 for field in path.iter().rev() {
127 current = serde_json::json!({
128 "type": "object",
129 "properties": {
130 *field: current
131 }
132 });
133 }
134
135 if let Some(props) = current.get("properties") {
138 return props.clone();
139 }
140
141 current
142 }
143}
144
145impl GtsSchema for () {
147 const SCHEMA_ID: &'static str = "";
148
149 fn gts_schema_with_refs() -> Value {
150 serde_json::json!({
151 "type": "object"
152 })
153 }
154
155 fn gts_schema() -> Value {
156 Self::gts_schema_with_refs()
157 }
158}
159
160pub trait GtsSerialize {
166 fn gts_serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
172 where
173 S: serde::Serializer;
174}
175
176pub trait GtsDeserialize<'de>: Sized {
181 fn gts_deserialize<__D>(deserializer: __D) -> Result<Self, __D::Error>
187 where
188 __D: serde::Deserializer<'de>;
189}
190
191#[doc(hidden)]
196pub trait GtsNoDirectSerialize {}
197
198#[doc(hidden)]
200pub trait GtsNoDirectDeserialize {}
201
202impl<T: serde::Serialize> GtsNoDirectSerialize for T {}
203
204impl<T> GtsNoDirectDeserialize for T where for<'de> T: serde::Deserialize<'de> {}
205
206impl<T: serde::Serialize> GtsSerialize for T {
209 fn gts_serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
210 where
211 S: serde::Serializer,
212 {
213 serde::Serialize::serialize(self, serializer)
214 }
215}
216
217impl<'de, T: serde::Deserialize<'de>> GtsDeserialize<'de> for T {
219 fn gts_deserialize<__D>(deserializer: __D) -> Result<Self, __D::Error>
220 where
221 __D: serde::Deserializer<'de>,
222 {
223 <T as serde::Deserialize<'de>>::deserialize(deserializer)
224 }
225}
226
227pub fn serialize_gts<T: GtsSerialize, S: serde::Serializer>(
235 value: &T,
236 serializer: S,
237) -> Result<S::Ok, S::Error> {
238 value.gts_serialize(serializer)
239}
240
241pub fn deserialize_gts<'de, T: GtsDeserialize<'de>, D: serde::Deserializer<'de>>(
249 deserializer: D,
250) -> Result<T, D::Error> {
251 T::gts_deserialize(deserializer)
252}
253
254#[doc(hidden)]
260pub struct GtsSerializeWrapper<'a, T: GtsSerialize + ?Sized>(pub &'a T);
261
262impl<T: GtsSerialize + ?Sized> serde::Serialize for GtsSerializeWrapper<'_, T> {
263 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
264 where
265 S: serde::Serializer,
266 {
267 self.0.gts_serialize(serializer)
268 }
269}
270
271#[doc(hidden)]
275pub struct GtsDeserializeWrapper<T>(pub T);
276
277impl<'de, T: GtsDeserialize<'de>> serde::Deserialize<'de> for GtsDeserializeWrapper<T> {
278 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
279 where
280 D: serde::Deserializer<'de>,
281 {
282 T::gts_deserialize(deserializer).map(GtsDeserializeWrapper)
283 }
284}
285
286#[macro_export]
309macro_rules! gts_schema_for {
310 ($base:ty) => {{
311 use $crate::GtsSchema;
312 <$base as GtsSchema>::gts_schema_with_refs_allof()
313 }};
314}
315
316#[must_use]
318pub fn strip_schema_metadata(schema: &Value) -> Value {
319 let mut result = schema.clone();
320 if let Some(obj) = result.as_object_mut() {
321 obj.remove("$id");
322 obj.remove("$schema");
323 obj.remove("title");
324 obj.remove("description");
325
326 if let Some(props) = obj.get_mut("properties").and_then(|v| v.as_object_mut()) {
328 let keys: Vec<String> = props.keys().cloned().collect();
329 for key in keys {
330 if let Some(prop_value) = props.get(&key) {
331 let cleaned = strip_schema_metadata(prop_value);
332 props.insert(key, cleaned);
333 }
334 }
335 }
336 }
337 result
338}
339
340#[must_use]
349pub fn build_gts_allof_schema(
350 innermost_schema_id: &str,
351 base_schema_id: &str,
352 title: &str,
353 own_properties: &Value,
354 required: &[&str],
355) -> Value {
356 serde_json::json!({
357 "$id": format!("gts://{}", innermost_schema_id),
358 "$schema": "http://json-schema.org/draft-07/schema#",
359 "title": title,
360 "type": "object",
361 "allOf": [
362 { "$ref": format!("gts://{}", base_schema_id) },
363 {
364 "type": "object",
365 "properties": own_properties,
366 "required": required
367 }
368 ]
369 })
370}
371
372#[cfg(test)]
373#[allow(clippy::unwrap_used, clippy::expect_used)]
374mod tests {
375 use super::*;
376 use serde_json::json;
377
378 #[test]
379 fn test_unit_type_properties() {
380 let schema = <()>::gts_schema();
382 assert_eq!(schema, json!({"type": "object"}));
383 assert_eq!(<()>::SCHEMA_ID, "");
384 assert_eq!(<()>::GENERIC_FIELD, None);
385 }
386
387 #[test]
388 fn test_wrap_in_nesting_path_empty_path() {
389 let properties = json!({"field1": {"type": "string"}});
390 let required = json!(["field1"]);
391
392 let result = <()>::wrap_in_nesting_path(&[], properties.clone(), required, None);
393
394 assert_eq!(result, properties);
395 }
396
397 #[test]
398 fn test_wrap_in_nesting_path_single_level() {
399 let properties = json!({"field1": {"type": "string"}});
400 let required = json!(["field1"]);
401
402 let result = <()>::wrap_in_nesting_path(&["payload"], properties, required.clone(), None);
403
404 assert_eq!(
405 result,
406 json!({
407 "payload": {
408 "type": "object",
409 "additionalProperties": false,
410 "properties": {"field1": {"type": "string"}},
411 "required": required
412 }
413 })
414 );
415 }
416
417 #[test]
418 fn test_wrap_in_nesting_path_multi_level() {
419 let properties = json!({"field1": {"type": "string"}});
420 let required = json!(["field1"]);
421
422 let result =
423 <()>::wrap_in_nesting_path(&["payload", "data"], properties, required.clone(), None);
424
425 assert_eq!(
426 result,
427 json!({
428 "payload": {
429 "type": "object",
430 "properties": {
431 "data": {
432 "type": "object",
433 "additionalProperties": false,
434 "properties": {"field1": {"type": "string"}},
435 "required": required
436 }
437 }
438 }
439 })
440 );
441 }
442
443 #[test]
444 fn test_wrap_in_nesting_path_with_generic_field() {
445 let properties = json!({
446 "field1": {"type": "string"},
447 "generic_field": {"type": "number"}
448 });
449 let required = json!(["field1"]);
450
451 let result =
452 <()>::wrap_in_nesting_path(&["payload"], properties, required, Some("generic_field"));
453
454 let result_obj = result.as_object().unwrap();
455 let payload = result_obj.get("payload").unwrap();
456 let props = payload.get("properties").unwrap();
457
458 assert_eq!(
460 props.get("generic_field").unwrap(),
461 &json!({"type": "object"})
462 );
463 assert_eq!(props.get("field1").unwrap(), &json!({"type": "string"}));
465 }
466
467 #[test]
468 fn test_strip_schema_metadata_removes_all_metadata() {
469 let schema = json!({
471 "$id": "gts://test",
472 "$schema": "http://json-schema.org/draft-07/schema#",
473 "title": "Test Schema",
474 "description": "A test",
475 "type": "object",
476 "properties": {"field": {"type": "string"}}
477 });
478
479 let result = strip_schema_metadata(&schema);
480
481 assert!(result.get("$id").is_none());
483 assert!(result.get("$schema").is_none());
484 assert!(result.get("title").is_none());
485 assert!(result.get("description").is_none());
486 assert_eq!(result.get("type").unwrap(), "object");
488 assert!(result.get("properties").is_some());
489 }
490
491 #[test]
492 fn test_strip_schema_metadata_recursive() {
493 let schema = json!({
494 "$id": "gts://test",
495 "properties": {
496 "nested": {
497 "$id": "gts://nested",
498 "type": "string",
499 "description": "Nested field"
500 }
501 }
502 });
503
504 let result = strip_schema_metadata(&schema);
505
506 assert!(result.get("$id").is_none());
507 let props = result.get("properties").unwrap();
508 let nested = props.get("nested").unwrap();
509 assert!(nested.get("$id").is_none());
510 assert!(nested.get("description").is_none());
511 assert_eq!(nested.get("type").unwrap(), "string");
512 }
513
514 #[test]
515 fn test_strip_schema_metadata_preserves_non_metadata() {
516 let schema = json!({
517 "$id": "gts://test",
518 "type": "object",
519 "properties": {"field": {"type": "string"}},
520 "required": ["field"],
521 "additionalProperties": false
522 });
523
524 let result = strip_schema_metadata(&schema);
525
526 assert_eq!(result.get("type").unwrap(), "object");
527 assert!(result.get("properties").is_some());
528 assert!(result.get("required").is_some());
529 assert_eq!(result.get("additionalProperties").unwrap(), &json!(false));
530 }
531
532 #[test]
533 fn test_build_gts_allof_schema_structure() {
534 let properties = json!({"field1": {"type": "string"}});
535 let required = vec!["field1"];
536
537 let result = build_gts_allof_schema(
538 "vendor.package.namespace.child.1",
539 "vendor.package.namespace.base.1",
540 "Child Schema",
541 &properties,
542 &required,
543 );
544
545 assert_eq!(
546 result.get("$id").unwrap(),
547 "gts://vendor.package.namespace.child.1"
548 );
549 assert_eq!(
550 result.get("$schema").unwrap(),
551 "http://json-schema.org/draft-07/schema#"
552 );
553 assert_eq!(result.get("title").unwrap(), "Child Schema");
554 assert_eq!(result.get("type").unwrap(), "object");
555
556 let allof = result.get("allOf").unwrap().as_array().unwrap();
557 assert_eq!(allof.len(), 2);
558 }
559
560 #[test]
561 fn test_build_gts_allof_schema_ref_format() {
562 let properties = json!({"field1": {"type": "string"}});
563 let required = vec!["field1"];
564
565 let result = build_gts_allof_schema(
566 "vendor.package.namespace.child.1",
567 "vendor.package.namespace.base.1",
568 "Child Schema",
569 &properties,
570 &required,
571 );
572
573 let allof = result.get("allOf").unwrap().as_array().unwrap();
574 let ref_obj = &allof[0];
575
576 assert_eq!(
577 ref_obj.get("$ref").unwrap(),
578 "gts://vendor.package.namespace.base.1"
579 );
580 }
581
582 #[test]
583 fn test_build_gts_allof_schema_properties_in_allof() {
584 let properties = json!({"field1": {"type": "string"}, "field2": {"type": "number"}});
585 let required = vec!["field1", "field2"];
586
587 let result = build_gts_allof_schema(
588 "vendor.package.namespace.child.1",
589 "vendor.package.namespace.base.1",
590 "Child Schema",
591 &properties,
592 &required,
593 );
594
595 let allof = result.get("allOf").unwrap().as_array().unwrap();
596 let props_obj = &allof[1];
597
598 assert_eq!(props_obj.get("type").unwrap(), "object");
599 assert_eq!(props_obj.get("properties").unwrap(), &properties);
600 assert_eq!(props_obj.get("required").unwrap(), &json!(required));
601 }
602}