use serde_json::Value;
pub trait GtsSchema {
const SCHEMA_ID: &'static str;
const GENERIC_FIELD: Option<&'static str> = None;
fn gts_schema_with_refs() -> Value;
#[must_use]
fn gts_schema() -> Value {
Self::gts_schema_with_refs()
}
#[must_use]
fn gts_schema_with_refs_allof() -> Value {
Self::gts_schema_with_refs()
}
#[must_use]
fn innermost_schema_id() -> &'static str {
Self::SCHEMA_ID
}
#[must_use]
fn innermost_schema() -> Value {
Self::gts_schema_with_refs()
}
#[must_use]
fn collect_nesting_path() -> Vec<&'static str> {
Vec::new()
}
#[must_use]
fn wrap_in_nesting_path(
path: &[&str],
properties: Value,
required: Value,
generic_field: Option<&str>,
) -> Value {
if path.is_empty() {
return properties;
}
let mut current = serde_json::json!({
"type": "object",
"additionalProperties": false,
"properties": properties,
"required": required
});
if let Some(gf) = generic_field
&& let Some(props) = current
.get_mut("properties")
.and_then(|v| v.as_object_mut())
&& props.contains_key(gf)
{
props.insert(gf.to_owned(), serde_json::json!({"type": "object"}));
}
for field in path.iter().rev() {
current = serde_json::json!({
"type": "object",
"properties": {
*field: current
}
});
}
if let Some(props) = current.get("properties") {
return props.clone();
}
current
}
}
impl GtsSchema for () {
const SCHEMA_ID: &'static str = "";
fn gts_schema_with_refs() -> Value {
serde_json::json!({
"type": "object"
})
}
fn gts_schema() -> Value {
Self::gts_schema_with_refs()
}
}
pub trait GtsSerialize {
fn gts_serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer;
}
pub trait GtsDeserialize<'de>: Sized {
fn gts_deserialize<__D>(deserializer: __D) -> Result<Self, __D::Error>
where
__D: serde::Deserializer<'de>;
}
#[doc(hidden)]
pub trait GtsNoDirectSerialize {}
#[doc(hidden)]
pub trait GtsNoDirectDeserialize {}
impl<T: serde::Serialize> GtsNoDirectSerialize for T {}
impl<T> GtsNoDirectDeserialize for T where for<'de> T: serde::Deserialize<'de> {}
impl<T: serde::Serialize> GtsSerialize for T {
fn gts_serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serde::Serialize::serialize(self, serializer)
}
}
impl<'de, T: serde::Deserialize<'de>> GtsDeserialize<'de> for T {
fn gts_deserialize<__D>(deserializer: __D) -> Result<Self, __D::Error>
where
__D: serde::Deserializer<'de>,
{
<T as serde::Deserialize<'de>>::deserialize(deserializer)
}
}
pub fn serialize_gts<T: GtsSerialize, S: serde::Serializer>(
value: &T,
serializer: S,
) -> Result<S::Ok, S::Error> {
value.gts_serialize(serializer)
}
pub fn deserialize_gts<'de, T: GtsDeserialize<'de>, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<T, D::Error> {
T::gts_deserialize(deserializer)
}
#[doc(hidden)]
pub struct GtsSerializeWrapper<'a, T: GtsSerialize + ?Sized>(pub &'a T);
impl<T: GtsSerialize + ?Sized> serde::Serialize for GtsSerializeWrapper<'_, T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.gts_serialize(serializer)
}
}
#[doc(hidden)]
pub struct GtsDeserializeWrapper<T>(pub T);
impl<'de, T: GtsDeserialize<'de>> serde::Deserialize<'de> for GtsDeserializeWrapper<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
T::gts_deserialize(deserializer).map(GtsDeserializeWrapper)
}
}
#[macro_export]
macro_rules! gts_schema_for {
($base:ty) => {{
use $crate::GtsSchema;
<$base as GtsSchema>::gts_schema_with_refs_allof()
}};
}
#[must_use]
pub fn strip_schema_metadata(schema: &Value) -> Value {
let mut result = schema.clone();
if let Some(obj) = result.as_object_mut() {
obj.remove("$id");
obj.remove("$schema");
obj.remove("title");
obj.remove("description");
if let Some(props) = obj.get_mut("properties").and_then(|v| v.as_object_mut()) {
let keys: Vec<String> = props.keys().cloned().collect();
for key in keys {
if let Some(prop_value) = props.get(&key) {
let cleaned = strip_schema_metadata(prop_value);
props.insert(key, cleaned);
}
}
}
}
result
}
#[must_use]
pub fn build_gts_allof_schema(
innermost_schema_id: &str,
base_schema_id: &str,
title: &str,
own_properties: &Value,
required: &[&str],
) -> Value {
serde_json::json!({
"$id": format!("gts://{}", innermost_schema_id),
"$schema": "http://json-schema.org/draft-07/schema#",
"title": title,
"type": "object",
"allOf": [
{ "$ref": format!("gts://{}", base_schema_id) },
{
"type": "object",
"properties": own_properties,
"required": required
}
]
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_unit_type_properties() {
let schema = <()>::gts_schema();
assert_eq!(schema, json!({"type": "object"}));
assert_eq!(<()>::SCHEMA_ID, "");
assert_eq!(<()>::GENERIC_FIELD, None);
}
#[test]
fn test_wrap_in_nesting_path_empty_path() {
let properties = json!({"field1": {"type": "string"}});
let required = json!(["field1"]);
let result = <()>::wrap_in_nesting_path(&[], properties.clone(), required, None);
assert_eq!(result, properties);
}
#[test]
fn test_wrap_in_nesting_path_single_level() {
let properties = json!({"field1": {"type": "string"}});
let required = json!(["field1"]);
let result = <()>::wrap_in_nesting_path(&["payload"], properties, required.clone(), None);
assert_eq!(
result,
json!({
"payload": {
"type": "object",
"additionalProperties": false,
"properties": {"field1": {"type": "string"}},
"required": required
}
})
);
}
#[test]
fn test_wrap_in_nesting_path_multi_level() {
let properties = json!({"field1": {"type": "string"}});
let required = json!(["field1"]);
let result =
<()>::wrap_in_nesting_path(&["payload", "data"], properties, required.clone(), None);
assert_eq!(
result,
json!({
"payload": {
"type": "object",
"properties": {
"data": {
"type": "object",
"additionalProperties": false,
"properties": {"field1": {"type": "string"}},
"required": required
}
}
}
})
);
}
#[test]
fn test_wrap_in_nesting_path_with_generic_field() {
let properties = json!({
"field1": {"type": "string"},
"generic_field": {"type": "number"}
});
let required = json!(["field1"]);
let result =
<()>::wrap_in_nesting_path(&["payload"], properties, required, Some("generic_field"));
let result_obj = result.as_object().unwrap();
let payload = result_obj.get("payload").unwrap();
let props = payload.get("properties").unwrap();
assert_eq!(
props.get("generic_field").unwrap(),
&json!({"type": "object"})
);
assert_eq!(props.get("field1").unwrap(), &json!({"type": "string"}));
}
#[test]
fn test_strip_schema_metadata_removes_all_metadata() {
let schema = json!({
"$id": "gts://test",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Test Schema",
"description": "A test",
"type": "object",
"properties": {"field": {"type": "string"}}
});
let result = strip_schema_metadata(&schema);
assert!(result.get("$id").is_none());
assert!(result.get("$schema").is_none());
assert!(result.get("title").is_none());
assert!(result.get("description").is_none());
assert_eq!(result.get("type").unwrap(), "object");
assert!(result.get("properties").is_some());
}
#[test]
fn test_strip_schema_metadata_recursive() {
let schema = json!({
"$id": "gts://test",
"properties": {
"nested": {
"$id": "gts://nested",
"type": "string",
"description": "Nested field"
}
}
});
let result = strip_schema_metadata(&schema);
assert!(result.get("$id").is_none());
let props = result.get("properties").unwrap();
let nested = props.get("nested").unwrap();
assert!(nested.get("$id").is_none());
assert!(nested.get("description").is_none());
assert_eq!(nested.get("type").unwrap(), "string");
}
#[test]
fn test_strip_schema_metadata_preserves_non_metadata() {
let schema = json!({
"$id": "gts://test",
"type": "object",
"properties": {"field": {"type": "string"}},
"required": ["field"],
"additionalProperties": false
});
let result = strip_schema_metadata(&schema);
assert_eq!(result.get("type").unwrap(), "object");
assert!(result.get("properties").is_some());
assert!(result.get("required").is_some());
assert_eq!(result.get("additionalProperties").unwrap(), &json!(false));
}
#[test]
fn test_build_gts_allof_schema_structure() {
let properties = json!({"field1": {"type": "string"}});
let required = vec!["field1"];
let result = build_gts_allof_schema(
"vendor.package.namespace.child.1",
"vendor.package.namespace.base.1",
"Child Schema",
&properties,
&required,
);
assert_eq!(
result.get("$id").unwrap(),
"gts://vendor.package.namespace.child.1"
);
assert_eq!(
result.get("$schema").unwrap(),
"http://json-schema.org/draft-07/schema#"
);
assert_eq!(result.get("title").unwrap(), "Child Schema");
assert_eq!(result.get("type").unwrap(), "object");
let allof = result.get("allOf").unwrap().as_array().unwrap();
assert_eq!(allof.len(), 2);
}
#[test]
fn test_build_gts_allof_schema_ref_format() {
let properties = json!({"field1": {"type": "string"}});
let required = vec!["field1"];
let result = build_gts_allof_schema(
"vendor.package.namespace.child.1",
"vendor.package.namespace.base.1",
"Child Schema",
&properties,
&required,
);
let allof = result.get("allOf").unwrap().as_array().unwrap();
let ref_obj = &allof[0];
assert_eq!(
ref_obj.get("$ref").unwrap(),
"gts://vendor.package.namespace.base.1"
);
}
#[test]
fn test_build_gts_allof_schema_properties_in_allof() {
let properties = json!({"field1": {"type": "string"}, "field2": {"type": "number"}});
let required = vec!["field1", "field2"];
let result = build_gts_allof_schema(
"vendor.package.namespace.child.1",
"vendor.package.namespace.base.1",
"Child Schema",
&properties,
&required,
);
let allof = result.get("allOf").unwrap().as_array().unwrap();
let props_obj = &allof[1];
assert_eq!(props_obj.get("type").unwrap(), "object");
assert_eq!(props_obj.get("properties").unwrap(), &properties);
assert_eq!(props_obj.get("required").unwrap(), &json!(required));
}
}