1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
use serde_json::{json, Value};
use std::hash::Hasher;
use crate::genson_rs::node::{DataType, SchemaNode};
const DEFAULT_SCHEMA_URI: &str = "http://json-schema.org/schema#";
const NULL_SCHEMA_URI: &str = "NULL";
pub struct SchemaBuilder {
schema_uri: Option<String>,
root_node: SchemaNode,
}
impl SchemaBuilder {
/// Create a new SchemaBuilder object. The schema_uri parameter is optional, a value
/// of "AUTO" will automatically detect the schema URI based on the input schema, if no
/// schema URI was detected, a default URI of "<http://json-schema.org/schema#>" will be used.
/// A value of None will leave out the "$schema" keyword in the output schema.
pub fn new(schema_uri: Option<&str>) -> Self {
// TODO: the functionality to allow non-default node class with extended
// strategies is not supported yet
let root_node: SchemaNode = SchemaNode::new();
if let Some(uri) = schema_uri {
if uri == "AUTO" {
SchemaBuilder {
schema_uri: None,
root_node,
}
} else {
SchemaBuilder {
schema_uri: Some(uri.to_string()),
root_node,
}
}
} else {
SchemaBuilder {
schema_uri: Some(NULL_SCHEMA_URI.to_string()),
root_node,
}
}
}
/// Merge in raw JSON schema object
pub fn add_schema(&mut self, mut schema: Value) {
self.add_schema_mut(&mut schema);
}
/// Like [`add_schema`](Self::add_schema) but leaves `schema` with the caller, so it can be
/// dropped elsewhere (a large schema is costly to free). Removes `$schema` from it when
/// that is adopted as this builder's schema URI.
pub fn add_schema_mut(&mut self, schema: &mut Value) {
if let Value::Object(schema_obj) = schema {
if schema_obj.contains_key("$schema") && self.schema_uri.is_none() {
self.schema_uri = Some(schema_obj["$schema"].as_str().unwrap().to_string());
schema_obj.shift_remove("$schema");
}
self.root_node.add_schema(DataType::Schema(schema));
} else {
panic!("Invalid schema type - must be a valid JSON object")
}
}
/// Merge `schemas` in order, same result as calling [`add_schema_mut`](Self::add_schema_mut)
/// on each, with the merge parallelised across the root object's properties.
pub fn add_schemas_mut(&mut self, schemas: &mut [Value]) {
for schema in schemas.iter_mut() {
let Value::Object(schema_obj) = schema else {
panic!("Invalid schema type - must be a valid JSON object")
};
if schema_obj.contains_key("$schema") && self.schema_uri.is_none() {
self.schema_uri = Some(schema_obj["$schema"].as_str().unwrap().to_string());
schema_obj.shift_remove("$schema");
}
}
let refs: Vec<&Value> = schemas.iter().collect();
if !self.root_node.add_object_schemas_par(&refs) {
for schema in refs {
self.root_node.add_schema(DataType::Schema(schema));
}
}
}
/// Merge another builder into this one. When this builder is still empty and both
/// share the same `$schema` handling, takes over its nodes instead of round-tripping
/// through a schema value.
pub fn absorb(&mut self, other: SchemaBuilder) {
// `other` with the NULL uri emits no "$schema", so nothing else would be merged.
if self.root_node.is_empty() && other.schema_uri.as_deref() == Some(NULL_SCHEMA_URI) {
self.root_node = other.root_node;
} else {
self.add_builder(other);
}
}
/// Merge another builder into this one, with the same result as
/// `self.add_schema(other.to_schema())` but merging its nodes directly.
pub fn add_builder(&mut self, other: SchemaBuilder) {
match other.emitted_schema_uri() {
None => self.root_node.absorb(other.root_node),
// `add_schema_mut` adopts the `$schema` and merges the rest
Some(uri) if self.schema_uri.is_none() => {
self.schema_uri = Some(uri.to_string());
self.root_node.absorb(other.root_node);
}
// Otherwise the `$schema` keyword itself is merged into the root
Some(_) => self.add_schema(other.to_schema()),
}
}
/// Merge `others` in order, with the same result as `add_builder` on each in turn but
/// with the roots that are objects merged in parallel across their properties.
pub fn add_builders(&mut self, others: Vec<SchemaBuilder>) {
let mut objects = Vec::with_capacity(others.len());
let mut kept_uri = None;
for other in others {
if !other.root_node.is_object() {
self.root_node.absorb_all(std::mem::take(&mut objects));
self.add_builder(other);
continue;
}
match (other.emitted_schema_uri(), &self.schema_uri) {
(Some(uri), None) => self.schema_uri = Some(uri.to_string()),
// `add_schema` merges an unadopted `$schema` into the root as a keyword
(Some(uri), Some(_)) => kept_uri = kept_uri.or(Some(uri.to_string())),
(None, _) => {}
}
objects.push(other.root_node);
}
self.root_node.absorb_all(objects);
if let Some(uri) = kept_uri {
self.root_node.add_object_keyword("$schema", uri.into());
}
}
pub(crate) fn root_node_mut(&mut self) -> &mut SchemaNode {
&mut self.root_node
}
/// A hash of the schema `to_schema` gives, computed without building it: builders with
/// equal schemas hash equal.
pub fn schema_hash(&self) -> u64 {
let mut hasher = xxhash_rust::xxh64::Xxh64::new(0);
hasher.write(self.emitted_schema_uri().unwrap_or_default().as_bytes());
hasher.write_u8(0xff);
self.root_node.hash_schema(&mut hasher);
hasher.finish()
}
/// The `$schema` that `to_schema` writes, if any.
fn emitted_schema_uri(&self) -> Option<&str> {
match self.schema_uri.as_deref() {
Some(NULL_SCHEMA_URI) => None,
Some(uri) => Some(uri),
None => Some(DEFAULT_SCHEMA_URI),
}
}
/// Merge multiple raw JSON schema objects at once
pub fn add_schemas(&mut self, schemas: &[Value]) {
let schemas: Vec<&Value> = schemas.iter().collect();
self.root_node.add_schemas(&schemas);
}
/// Merge in another SchemaNode object
pub fn add_schema_node(&mut self, node: SchemaNode) {
self.root_node.add_schema(DataType::SchemaNode(&node));
}
/// Modify the schema to accomodate the input object
pub fn add_object(&mut self, object: super::JsonValue) {
self.root_node.add_object(DataType::Object(object));
}
/// Export the currently constructed schema as a JSON object
pub fn to_schema(&self) -> Value {
let mut base_schema = self.get_base_schema();
let base_schema_map = base_schema.as_object_mut().unwrap();
let Value::Object(node_schema_map) = self.root_node.to_schema() else {
panic!("node schema must be an object")
};
for (key, value) in node_schema_map {
base_schema_map.insert(key, value);
}
base_schema
}
fn get_base_schema(&self) -> Value {
if let Some(uri) = &self.schema_uri {
if uri == NULL_SCHEMA_URI {
json!({})
} else {
json!({"$schema": uri})
}
} else {
json!({"$schema": DEFAULT_SCHEMA_URI})
}
}
/// Serialize the currently constructed schema to a JSON string
pub fn to_json(&self) -> String {
let schema = self.to_schema();
schema.to_string()
}
}