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