1use serde_json::{Map, Value};
2
3use super::types::{DefaultValue, FieldSpec, IndexKind, TableSchema, SCHEMA_VERSION};
4
5fn index_kind_str(kind: IndexKind) -> &'static str {
6 match kind {
7 IndexKind::Exact => "exact",
8 IndexKind::Sorted => "sorted",
9 }
10}
11
12pub fn field_document(spec: &FieldSpec) -> Value {
20 let mut doc = Map::new();
21 doc.insert("type".to_string(), Value::from(spec.field_type.as_str()));
22 if spec.required {
23 doc.insert("required".to_string(), Value::Bool(true));
24 }
25 if spec.unique {
26 doc.insert("unique".to_string(), Value::Bool(true));
27 }
28 if spec.fulltext {
29 doc.insert("fulltext".to_string(), Value::Bool(true));
30 }
31 if spec.cypher {
32 doc.insert("cypher".to_string(), Value::Bool(true));
33 }
34 if spec.password_hash {
35 doc.insert("passwordHash".to_string(), Value::Bool(true));
36 }
37 if !spec.addressable {
39 doc.insert("addressable".to_string(), Value::Bool(false));
40 }
41 if let Some(index) = spec.index {
42 doc.insert("index".to_string(), Value::from(index_kind_str(index)));
43 }
44 if let Some(values) = &spec.values {
45 doc.insert("values".to_string(), Value::from(values.clone()));
46 }
47 match &spec.default {
48 Some(DefaultValue::Now) => {
49 doc.insert("default".to_string(), Value::from("now"));
50 }
51 Some(DefaultValue::Value(value)) => {
52 doc.insert("default".to_string(), value.clone());
53 }
54 None => {}
55 }
56 Value::Object(doc)
57}
58
59pub fn schema_document(schema: &TableSchema) -> Value {
63 let mut doc = Map::new();
64 doc.insert("schemaVersion".to_string(), Value::from(SCHEMA_VERSION));
65 doc.insert("table".to_string(), Value::from(schema.table.clone()));
66 doc.insert("revision".to_string(), Value::from(schema.revision));
67 doc.insert("primaryKey".to_string(), Value::from(schema.primary_key.clone()));
68 if let Some(partition_by) = &schema.partition_by {
69 doc.insert("partitionBy".to_string(), Value::from(partition_by.clone()));
70 }
71 let mut fields = Map::new();
72 for (name, spec) in &schema.fields {
73 fields.insert(name.clone(), field_document(spec));
74 }
75 doc.insert("fields".to_string(), Value::Object(fields));
76 Value::Object(doc)
77}
78
79#[cfg(test)]
80mod tests {
81 use serde_json::json;
82
83 use super::super::parse::parse_schema;
84 use super::*;
85
86 fn parsed(document: Value) -> TableSchema {
87 parse_schema(&document).expect("the fixture is a usable schema")
88 }
89
90 #[test]
91 fn renders_only_the_modifiers_a_field_actually_carries() {
92 let schema = parsed(json!({
93 "schemaVersion": 1,
94 "table": "users",
95 "primaryKey": "id",
96 "fields": {
97 "id": { "type": "string" },
98 "bio": { "type": "string" },
99 },
100 }));
101
102 let document = schema_document(&schema);
103
104 assert_eq!(document["fields"]["bio"], json!({ "type": "string" }));
105 assert_eq!(
106 document["fields"]["id"],
107 json!({ "type": "string", "required": true, "unique": true, "index": "exact" })
108 );
109 }
110
111 #[test]
112 fn keeps_the_key_order_the_ts_document_has() {
113 let schema = parsed(json!({
114 "schemaVersion": 1,
115 "table": "users",
116 "revision": 4,
117 "primaryKey": "id",
118 "partitionBy": "tenant",
119 "fields": {
120 "id": { "type": "string" },
121 "tenant": { "type": "string", "required": true },
122 "role": { "type": "enum", "values": ["admin"], "index": "exact", "default": "admin" },
123 },
124 }));
125
126 let document = schema_document(&schema);
127
128 let keys: Vec<&String> = document.as_object().unwrap().keys().collect();
129 assert_eq!(
130 keys,
131 vec![
132 "schemaVersion",
133 "table",
134 "revision",
135 "primaryKey",
136 "partitionBy",
137 "fields"
138 ]
139 );
140 let role: Vec<&String> = document["fields"]["role"].as_object().unwrap().keys().collect();
141 assert_eq!(role, vec!["type", "index", "values", "default"]);
142 let fields: Vec<&String> = document["fields"].as_object().unwrap().keys().collect();
143 assert_eq!(fields, vec!["id", "role", "tenant"]);
144 }
145
146 #[test]
147 fn leaves_partition_by_out_of_a_table_that_declares_none() {
148 let schema = parsed(json!({
149 "schemaVersion": 1,
150 "table": "users",
151 "primaryKey": "id",
152 "fields": { "id": { "type": "string" } },
153 }));
154
155 let document = schema_document(&schema);
156
157 assert!(document.as_object().unwrap().get("partitionBy").is_none());
158 assert_eq!(document["revision"], json!(1));
159 }
160
161 #[test]
162 fn renders_a_cypher_field_as_the_bare_fact_of_it() {
163 let schema = parsed(json!({
164 "schemaVersion": 1,
165 "table": "secrets",
166 "primaryKey": "id",
167 "fields": {
168 "id": { "type": "string" },
169 "token": { "type": "string", "cypher": true },
170 },
171 }));
172
173 let document = schema_document(&schema);
174
175 assert_eq!(document["fields"]["token"], json!({ "type": "string", "cypher": true }));
176 }
177
178 #[test]
179 fn round_trips_through_parse_schema() {
180 let schema = parsed(json!({
181 "schemaVersion": 1,
182 "table": "everything",
183 "revision": 2,
184 "primaryKey": "id",
185 "partitionBy": "tenant",
186 "fields": {
187 "id": { "type": "string" },
188 "tenant": { "type": "string", "required": true },
189 "page": { "type": "markdown" },
190 "cover": { "type": "image" },
191 "tags": { "type": "string[]", "index": "exact", "default": ["new"] },
192 "seenAt": { "type": "timestamp", "default": "now", "index": "sorted" },
193 "live": { "type": "bool", "default": false },
194 "notes": { "type": "string", "fulltext": true },
195 "slug": { "type": "string", "unique": true },
196 },
197 }));
198
199 let again = parse_schema(&schema_document(&schema)).expect("the rendered document parses");
200
201 assert_eq!(again, schema);
202 }
203}