1use std::collections::BTreeMap;
2
3use serde_json::{Map, Value};
4
5use crate::errors::HaysonSchemaError;
6
7use super::types::{DefaultValue, FieldSpec, FieldType, IndexKind, TableSchema, SCHEMA_VERSION};
8
9const FIELD_SPEC_KEYS: &[&str] = &[
10 "type",
11 "required",
12 "unique",
13 "index",
14 "fulltext",
15 "cypher",
16 "values",
17 "default",
18 "addressable",
19];
20const SCHEMA_KEYS: &[&str] = &[
21 "schemaVersion",
22 "table",
23 "revision",
24 "primaryKey",
25 "partitionBy",
26 "fields",
27];
28
29fn fail(message: impl Into<String>) -> HaysonSchemaError {
30 HaysonSchemaError(message.into())
31}
32
33fn is_table_name(name: &str) -> bool {
34 let mut chars = name.chars();
35 match chars.next() {
36 Some(c) if c.is_ascii_alphabetic() => {}
37 _ => return false,
38 }
39 chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
40}
41
42fn is_field_name(name: &str) -> bool {
43 let mut chars = name.chars();
44 match chars.next() {
45 Some(c) if c.is_ascii_alphabetic() => {}
46 _ => return false,
47 }
48 chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
49}
50
51fn as_object<'a>(value: &'a Value, what: &str) -> Result<&'a Map<String, Value>, HaysonSchemaError> {
52 value
53 .as_object()
54 .ok_or_else(|| fail(format!("{what} must be an object")))
55}
56
57fn field_type_of(raw: &str) -> Option<FieldType> {
58 Some(match raw {
59 "string" => FieldType::String,
60 "int" => FieldType::Int,
61 "float" => FieldType::Float,
62 "bool" => FieldType::Bool,
63 "timestamp" => FieldType::Timestamp,
64 "enum" => FieldType::Enum,
65 "json" => FieldType::Json,
66 "string[]" => FieldType::StringArray,
67 "int[]" => FieldType::IntArray,
68 "float[]" => FieldType::FloatArray,
69 "bool[]" => FieldType::BoolArray,
70 "markdown" => FieldType::Markdown,
71 "image" => FieldType::Image,
72 _ => return None,
73 })
74}
75
76fn parse_flag(source: &Map<String, Value>, key: &str, what: &str) -> Result<bool, HaysonSchemaError> {
77 match source.get(key) {
78 None => Ok(false),
79 Some(Value::Bool(b)) => Ok(*b),
80 Some(_) => Err(fail(format!("{what} must be a boolean"))),
81 }
82}
83
84fn parse_default(
85 raw: Option<&Value>,
86 field_type: FieldType,
87 what: &str,
88) -> Result<Option<DefaultValue>, HaysonSchemaError> {
89 let Some(raw) = raw else { return Ok(None) };
90 if let Value::String(s) = raw {
91 if s == "now" {
92 if field_type != FieldType::Timestamp {
93 return Err(fail(format!("{what} may only be \"now\" on a timestamp field")));
94 }
95 return Ok(Some(DefaultValue::Now));
96 }
97 }
98 if raw.is_null() {
99 return Ok(Some(DefaultValue::Value(Value::Null)));
100 }
101 if raw.is_array() {
102 if !field_type.is_array() {
103 return Err(fail(format!(
104 "{what} must not be an array on a {} field",
105 field_type.as_str()
106 )));
107 }
108 return Ok(Some(DefaultValue::Value(raw.clone())));
109 }
110 if !(raw.is_string() || raw.is_number() || raw.is_boolean()) {
111 return Err(fail(format!("{what} must be a string, number, boolean, array or null")));
112 }
113 if field_type.is_array() {
114 return Err(fail(format!(
115 "{what} must be an array on a {} field",
116 field_type.as_str()
117 )));
118 }
119 Ok(Some(DefaultValue::Value(raw.clone())))
120}
121
122fn parse_field_spec(name: &str, raw: &Value) -> Result<FieldSpec, HaysonSchemaError> {
123 let source = as_object(raw, &format!("field \"{name}\""))?;
124 for key in source.keys() {
125 if !FIELD_SPEC_KEYS.contains(&key.as_str()) {
126 return Err(fail(format!("field \"{name}\" has an unknown option \"{key}\"")));
127 }
128 }
129
130 let type_raw = source
131 .get("type")
132 .and_then(Value::as_str)
133 .ok_or_else(|| fail(format!("field \"{name}\" has an unsupported type")))?;
134 let field_type = field_type_of(type_raw)
135 .ok_or_else(|| fail(format!("field \"{name}\" has an unsupported type {type_raw:?}")))?;
136
137 let index = match source.get("index") {
138 None => None,
139 Some(Value::String(s)) if s == "exact" => Some(IndexKind::Exact),
140 Some(Value::String(s)) if s == "sorted" => Some(IndexKind::Sorted),
141 Some(other) => return Err(fail(format!("field \"{name}\" has an unsupported index {other:?}"))),
142 };
143 if index == Some(IndexKind::Sorted) && !field_type.is_sortable() {
144 return Err(fail(format!(
145 "field \"{name}\" of type {} cannot carry a sorted index",
146 field_type.as_str()
147 )));
148 }
149
150 let fulltext = parse_flag(source, "fulltext", &format!("field \"{name}\" option \"fulltext\""))?;
151 if fulltext && field_type != FieldType::String && field_type != FieldType::StringArray {
152 return Err(fail(format!(
153 "field \"{name}\" of type {} cannot be indexed for full-text search",
154 field_type.as_str()
155 )));
156 }
157
158 let unique = parse_flag(source, "unique", &format!("field \"{name}\" option \"unique\""))?;
159 if unique && field_type.is_array() {
160 return Err(fail(format!(
161 "field \"{name}\" of type {} cannot be unique",
162 field_type.as_str()
163 )));
164 }
165
166 let cypher = parse_flag(source, "cypher", &format!("field \"{name}\" option \"cypher\""))?;
167 if cypher {
168 if index.is_some() {
170 return Err(fail(format!(
171 "encrypted field \"{name}\" cannot carry an index — a random IV makes two equal values differ"
172 )));
173 }
174 if unique {
175 return Err(fail(format!(
176 "encrypted field \"{name}\" cannot be unique — equal ciphertext is not equal values"
177 )));
178 }
179 if fulltext {
180 return Err(fail(format!(
181 "encrypted field \"{name}\" cannot be indexed for full-text search"
182 )));
183 }
184 if source.contains_key("default") {
185 return Err(fail(format!(
186 "encrypted field \"{name}\" cannot carry a default — it would sit in the schema in clear"
187 )));
188 }
189 }
190
191 let addressable = match source.get("addressable") {
192 None => true,
193 Some(_) if field_type != FieldType::Markdown => {
194 return Err(fail(format!(
195 "field \"{name}\" of type {} must not declare \"addressable\" — only a markdown field can opt out of a virtual path of its own",
196 field_type.as_str()
197 )))
198 }
199 Some(Value::Bool(flag)) => *flag,
200 Some(_) => {
201 return Err(fail(format!(
202 "field \"{name}\" option \"addressable\" must be a boolean"
203 )))
204 }
205 };
206
207 if field_type.has_virtual_path() {
208 if addressable {
209 if index.is_some() {
210 return Err(fail(format!(
211 "field \"{name}\" of type {} cannot carry an index — its virtual path is indexed by the engine",
212 field_type.as_str()
213 )));
214 }
215 if unique {
216 return Err(fail(format!(
217 "field \"{name}\" of type {} cannot declare \"unique\" — its virtual path is always unique",
218 field_type.as_str()
219 )));
220 }
221 } else {
222 if index.is_some() {
223 return Err(fail(format!(
224 "field \"{name}\" of type {} cannot carry an index — a whole markdown document is not a lookup key",
225 field_type.as_str()
226 )));
227 }
228 if unique {
229 return Err(fail(format!(
230 "field \"{name}\" of type {} cannot declare \"unique\" — a whole markdown document is not a key to be unique on",
231 field_type.as_str()
232 )));
233 }
234 }
235 if fulltext {
236 return Err(fail(format!(
237 "field \"{name}\" of type {} cannot be indexed for full-text search",
238 field_type.as_str()
239 )));
240 }
241 if cypher {
242 return Err(fail(format!(
243 "field \"{name}\" of type {} cannot be encrypted",
244 field_type.as_str()
245 )));
246 }
247 if source.contains_key("default") {
248 return Err(fail(format!(
249 "field \"{name}\" of type {} cannot carry a default",
250 field_type.as_str()
251 )));
252 }
253 }
254
255 let values = if field_type == FieldType::Enum {
256 let raws = source
257 .get("values")
258 .and_then(Value::as_array)
259 .filter(|v| !v.is_empty())
260 .ok_or_else(|| fail(format!("enum field \"{name}\" must list its \"values\"")))?;
261 let mut values = Vec::with_capacity(raws.len());
262 for raw in raws {
263 let s = raw
264 .as_str()
265 .ok_or_else(|| fail(format!("enum field \"{name}\" must list string \"values\"")))?;
266 values.push(s.to_string());
267 }
268 let unique_count = values.iter().collect::<std::collections::HashSet<_>>().len();
269 if unique_count != values.len() {
270 return Err(fail(format!("enum field \"{name}\" has duplicate values")));
271 }
272 Some(values)
273 } else {
274 if source.contains_key("values") {
275 return Err(fail(format!(
276 "field \"{name}\" of type {} must not declare \"values\"",
277 field_type.as_str()
278 )));
279 }
280 None
281 };
282
283 let mut spec = FieldSpec::new(field_type);
284 spec.required = parse_flag(source, "required", &format!("field \"{name}\" option \"required\""))?;
285 spec.unique = unique;
286 spec.fulltext = fulltext;
287 spec.cypher = cypher;
288 spec.addressable = addressable;
289 spec.index = if unique {
291 Some(index.unwrap_or(IndexKind::Exact))
292 } else {
293 index
294 };
295 spec.values = values;
296
297 let default = parse_default(
298 source.get("default"),
299 field_type,
300 &format!("field \"{name}\" option \"default\""),
301 )?;
302 if let Some(DefaultValue::Value(ref value)) = default {
303 if let Some(values) = &spec.values {
304 if !value.is_null() {
305 let as_text = match value {
306 Value::String(s) => s.clone(),
307 other => other.to_string(),
308 };
309 if !values.contains(&as_text) {
310 return Err(fail(format!(
311 "enum field \"{name}\" has a default outside of its values"
312 )));
313 }
314 }
315 }
316 }
317 spec.default = default;
318
319 if field_type.is_array() {
320 if let Some(DefaultValue::Value(Value::Array(items))) = &spec.default {
321 let element = field_type.element_type().expect("array type has an element type");
322 let bad = items.iter().any(|item| match element {
323 FieldType::String => !item.is_string(),
324 FieldType::Bool => !item.is_boolean(),
325 _ => !item.is_number(),
326 });
327 if bad {
328 return Err(fail(format!(
329 "field \"{name}\" has a default that does not match {}",
330 field_type.as_str()
331 )));
332 }
333 }
334 }
335
336 Ok(spec)
337}
338
339fn parse_partition_by(
340 raw: Option<&Value>,
341 table: &str,
342 primary_key: &str,
343 fields: &BTreeMap<String, FieldSpec>,
344) -> Result<Option<String>, HaysonSchemaError> {
345 let Some(raw) = raw else { return Ok(None) };
346 let name = raw
347 .as_str()
348 .filter(|s| !s.is_empty())
349 .ok_or_else(|| fail(format!("\"partitionBy\" of table \"{table}\" must name a field")))?;
350 let spec = fields.get(name).ok_or_else(|| {
351 fail(format!(
352 "\"partitionBy\" of table \"{table}\" names an unknown field \"{name}\""
353 ))
354 })?;
355 if name == primary_key {
356 return Err(fail(format!(
357 "\"partitionBy\" of table \"{table}\" cannot be its primary key \"{name}\" — that is a file per record"
358 )));
359 }
360 if spec.field_type != FieldType::String {
361 return Err(fail(format!(
362 "partition field \"{name}\" of table \"{table}\" must be a string, not {}",
363 spec.field_type.as_str()
364 )));
365 }
366 if !spec.required {
367 return Err(fail(format!(
368 "partition field \"{name}\" of table \"{table}\" must be required — every record needs a partition"
369 )));
370 }
371 if spec.unique {
372 return Err(fail(format!(
373 "partition field \"{name}\" of table \"{table}\" cannot be unique — that is a file per record"
374 )));
375 }
376 if spec.cypher {
377 return Err(fail(format!(
378 "partition field \"{name}\" of table \"{table}\" cannot be encrypted — its value is the file name"
379 )));
380 }
381 Ok(Some(name.to_string()))
382}
383
384pub(crate) fn with_current_schema_marker(mut document: Value) -> Value {
390 if let Some(object) = document.as_object_mut() {
391 if !object.contains_key("schemaVersion") {
392 if let Some(marker) = object.remove("hayson") {
393 object.insert("schemaVersion".to_string(), marker);
394 }
395 }
396 }
397 document
398}
399
400pub fn parse_schema(input: &Value) -> Result<TableSchema, HaysonSchemaError> {
402 let source = as_object(input, "schema")?;
403
404 if source.get("schemaVersion") != Some(&Value::from(SCHEMA_VERSION)) {
405 return Err(fail(format!(
406 "schema must declare \"schemaVersion\": {SCHEMA_VERSION}, got {:?}",
407 source.get("schemaVersion")
408 )));
409 }
410
411 for key in source.keys() {
412 if !SCHEMA_KEYS.contains(&key.as_str()) {
413 return Err(fail(format!(
414 "schema of table {:?} has an unknown option \"{key}\" — it was most likely written by a newer version of hayson, and guessing what it means would read the table wrongly. Upgrade @theritual/hayson to open it.",
415 source.get("table").and_then(Value::as_str)
416 )));
417 }
418 }
419
420 let table = source
421 .get("table")
422 .and_then(Value::as_str)
423 .filter(|s| is_table_name(s))
424 .ok_or_else(|| fail(format!("schema has an unusable table name {:?}", source.get("table"))))?
425 .to_string();
426
427 let revision = match source.get("revision") {
428 None => 1,
429 Some(Value::Number(n)) if n.is_u64() && n.as_u64().unwrap() >= 1 => n.as_u64().unwrap() as u32,
430 _ => {
431 return Err(fail(format!(
432 "schema of table \"{table}\" must have a positive integer \"revision\""
433 )))
434 }
435 };
436
437 let primary_key = source
438 .get("primaryKey")
439 .and_then(Value::as_str)
440 .filter(|s| !s.is_empty())
441 .ok_or_else(|| fail(format!("schema of table \"{table}\" must name its \"primaryKey\"")))?
442 .to_string();
443
444 let raw_fields = as_object(
445 source.get("fields").unwrap_or(&Value::Null),
446 &format!("\"fields\" of table \"{table}\""),
447 )?;
448 if raw_fields.is_empty() {
449 return Err(fail(format!("schema of table \"{table}\" declares no fields")));
450 }
451
452 let mut fields = BTreeMap::new();
453 for (name, raw) in raw_fields {
454 if !is_field_name(name) {
455 return Err(fail(format!(
456 "field name {name:?} in table \"{table}\" is not usable (reserved or invalid)"
457 )));
458 }
459 fields.insert(name.clone(), parse_field_spec(name, raw)?);
460 }
461
462 {
463 let key = fields.get_mut(&primary_key).ok_or_else(|| {
464 fail(format!(
465 "primary key \"{primary_key}\" of table \"{table}\" is not among its fields"
466 ))
467 })?;
468 if key.field_type != FieldType::String && key.field_type != FieldType::Int {
469 return Err(fail(format!(
470 "primary key \"{primary_key}\" of table \"{table}\" must be a string or an int"
471 )));
472 }
473 if key.cypher {
474 return Err(fail(format!(
475 "primary key \"{primary_key}\" of table \"{table}\" cannot be encrypted — it is indexed by definition"
476 )));
477 }
478 key.required = true;
479 key.unique = true;
480 key.index = Some(IndexKind::Exact);
481 }
482
483 let partition_by = parse_partition_by(source.get("partitionBy"), &table, &primary_key, &fields)?;
484
485 Ok(TableSchema {
486 table,
487 revision,
488 primary_key,
489 partition_by,
490 fields,
491 })
492}
493
494#[cfg(test)]
495mod tests {
496 use std::fs;
497 use std::path::PathBuf;
498
499 use super::*;
500
501 fn fixture(name: &str) -> Value {
502 let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
503 path.push(format!("../../packages/hayson/test/fixtures/v1/{name}"));
504 let text = fs::read_to_string(&path).unwrap_or_else(|e| panic!("{path:?}: {e}"));
505 serde_json::from_str(&text).unwrap()
506 }
507
508 #[test]
509 fn parses_the_ts_reference_fixture_and_normalizes_the_primary_key() {
510 let schema = parse_schema(&with_current_schema_marker(fixture("unique-field/users.schema.json"))).unwrap();
512 assert_eq!(schema.table, "users");
513 assert_eq!(schema.primary_key, "id");
514 let id = &schema.fields["id"];
515 assert!(id.required);
516 assert!(id.unique);
517 assert_eq!(id.index, Some(IndexKind::Exact));
518
519 let email = &schema.fields["email"];
520 assert!(email.unique);
521 assert_eq!(email.index, Some(IndexKind::Exact));
522
523 let age = &schema.fields["age"];
524 assert_eq!(age.field_type, FieldType::Int);
525 assert_eq!(age.index, Some(IndexKind::Sorted));
526
527 let tags = &schema.fields["tags"];
528 assert_eq!(tags.field_type, FieldType::StringArray);
529 }
530
531 #[test]
534 fn rejects_a_schema_document_that_declares_the_pre_rename_marker() {
535 let doc = serde_json::json!({
536 "hayson": 1,
537 "table": "users",
538 "primaryKey": "id",
539 "fields": { "id": { "type": "string" } },
540 });
541 let failure = parse_schema(&doc).unwrap_err();
542 assert!(failure.0.contains("schemaVersion"), "{}", failure.0);
543 }
544
545 #[test]
546 fn moves_a_pre_rename_marker_onto_the_current_key_and_leaves_a_current_one_alone() {
547 let old = with_current_schema_marker(serde_json::json!({ "hayson": 1, "table": "users" }));
548 assert_eq!(old, serde_json::json!({ "table": "users", "schemaVersion": 1 }));
549
550 let current = serde_json::json!({ "schemaVersion": 1, "table": "users" });
551 assert_eq!(with_current_schema_marker(current.clone()), current);
552 }
553
554 #[test]
555 fn rejects_an_unknown_schema_option() {
556 let doc = serde_json::json!({
557 "schemaVersion": 1,
558 "table": "users",
559 "primaryKey": "id",
560 "fields": { "id": { "type": "string" } },
561 "somethingNew": true,
562 });
563 assert!(parse_schema(&doc).is_err());
564 }
565
566 #[test]
567 fn rejects_an_unknown_field_option() {
568 let doc = serde_json::json!({
569 "schemaVersion": 1,
570 "table": "users",
571 "primaryKey": "id",
572 "fields": { "id": { "type": "string", "wat": true } },
573 });
574 assert!(parse_schema(&doc).is_err());
575 }
576
577 #[test]
578 fn rejects_sorted_index_on_a_non_sortable_type() {
579 let doc = serde_json::json!({
580 "schemaVersion": 1,
581 "table": "users",
582 "primaryKey": "id",
583 "fields": {
584 "id": { "type": "string" },
585 "tags": { "type": "string[]", "index": "sorted" },
586 },
587 });
588 assert!(parse_schema(&doc).is_err());
589 }
590
591 #[test]
592 fn rejects_a_cypher_field_carrying_an_index() {
593 let doc = serde_json::json!({
594 "schemaVersion": 1,
595 "table": "users",
596 "primaryKey": "id",
597 "fields": {
598 "id": { "type": "string" },
599 "ssn": { "type": "string", "cypher": true, "index": "exact" },
600 },
601 });
602 assert!(parse_schema(&doc).is_err());
603 }
604
605 #[test]
606 fn rejects_partition_by_naming_the_primary_key() {
607 let doc = serde_json::json!({
608 "schemaVersion": 1,
609 "table": "notes",
610 "primaryKey": "id",
611 "partitionBy": "id",
612 "fields": {
613 "id": { "type": "string" },
614 },
615 });
616 assert!(parse_schema(&doc).is_err());
617 }
618
619 #[test]
620 fn parses_a_partitioned_schema() {
621 let doc = serde_json::json!({
622 "schemaVersion": 1,
623 "table": "notes",
624 "primaryKey": "id",
625 "partitionBy": "kind",
626 "fields": {
627 "id": { "type": "string" },
628 "kind": { "type": "string", "required": true, "index": "exact" },
629 "title": { "type": "string", "required": true, "fulltext": true },
630 },
631 });
632 let schema = parse_schema(&doc).unwrap();
633 assert_eq!(schema.partition_by, Some("kind".to_string()));
634 }
635
636 #[test]
637 fn accepts_markdown_and_image_field_declarations() {
638 let doc = serde_json::json!({
639 "schemaVersion": 1,
640 "table": "docs",
641 "primaryKey": "id",
642 "fields": {
643 "id": { "type": "string" },
644 "page": { "type": "markdown" },
645 "cover": { "type": "image" },
646 },
647 });
648 let schema = parse_schema(&doc).unwrap();
649 assert_eq!(schema.fields["page"].field_type, FieldType::Markdown);
650 assert_eq!(schema.fields["cover"].field_type, FieldType::Image);
651 }
652
653 #[test]
654 fn rejects_unique_declared_directly_on_a_markdown_field() {
655 let doc = serde_json::json!({
656 "schemaVersion": 1,
657 "table": "docs",
658 "primaryKey": "id",
659 "fields": {
660 "id": { "type": "string" },
661 "page": { "type": "markdown", "unique": true },
662 },
663 });
664 assert!(parse_schema(&doc).is_err());
665 }
666
667 #[test]
668 fn rejects_an_index_declared_directly_on_an_image_field() {
669 let doc = serde_json::json!({
670 "schemaVersion": 1,
671 "table": "docs",
672 "primaryKey": "id",
673 "fields": {
674 "id": { "type": "string" },
675 "cover": { "type": "image", "index": "exact" },
676 },
677 });
678 assert!(parse_schema(&doc).is_err());
679 }
680
681 #[test]
682 fn rejects_fulltext_declared_directly_on_a_markdown_field() {
683 let doc = serde_json::json!({
684 "schemaVersion": 1,
685 "table": "docs",
686 "primaryKey": "id",
687 "fields": {
688 "id": { "type": "string" },
689 "page": { "type": "markdown", "fulltext": true },
690 },
691 });
692 assert!(parse_schema(&doc).is_err());
693 }
694
695 fn docs_schema_with(page: serde_json::Value) -> Result<TableSchema, HaysonSchemaError> {
696 parse_schema(&serde_json::json!({
697 "schemaVersion": 1,
698 "table": "docs",
699 "primaryKey": "id",
700 "fields": {
701 "id": { "type": "string" },
702 "page": page,
703 },
704 }))
705 }
706
707 #[test]
708 fn a_markdown_field_is_addressable_unless_it_says_otherwise() {
709 let default = docs_schema_with(serde_json::json!({ "type": "markdown" })).unwrap();
710 let opted_out = docs_schema_with(serde_json::json!({ "type": "markdown", "addressable": false })).unwrap();
711 let opted_in = docs_schema_with(serde_json::json!({ "type": "markdown", "addressable": true })).unwrap();
712
713 assert!(default.fields["page"].addressable);
714 assert!(default.fields["page"].has_virtual_path());
715 assert!(!opted_out.fields["page"].addressable);
716 assert!(
717 !opted_out.fields["page"].has_virtual_path(),
718 "a field nothing can link to holds no virtual path of its own"
719 );
720 assert!(opted_in.fields["page"].has_virtual_path());
721 }
722
723 #[test]
724 fn only_a_markdown_field_may_declare_addressable() {
725 for field_type in ["image", "string", "json"] {
726 let refused = docs_schema_with(serde_json::json!({ "type": field_type, "addressable": false }))
727 .expect_err("only markdown opts out of a virtual path");
728
729 assert!(refused.0.contains("addressable"), "{}", refused.0);
730 assert!(refused.0.contains("markdown"), "{}", refused.0);
731 }
732 }
733
734 #[test]
735 fn addressable_has_to_be_a_boolean() {
736 let refused = docs_schema_with(serde_json::json!({ "type": "markdown", "addressable": "no" }))
737 .expect_err("\"no\" is not a boolean");
738
739 assert!(refused.0.contains("must be a boolean"), "{}", refused.0);
740 }
741
742 #[test]
743 fn a_non_addressable_markdown_field_is_refused_the_same_five_options_but_for_its_own_reason() {
744 let index = docs_schema_with(serde_json::json!({ "type": "markdown", "addressable": false, "index": "exact" }))
745 .expect_err("a whole markdown document is not a lookup key");
746 let unique = docs_schema_with(serde_json::json!({ "type": "markdown", "addressable": false, "unique": true }))
747 .expect_err("a whole markdown document is not a unique key");
748
749 assert!(
750 index.0.contains("not a lookup key"),
751 "the refusal must not still claim the field has an indexed virtual path: {}",
752 index.0
753 );
754 assert!(unique.0.contains("not a key to be unique on"), "{}", unique.0);
755 for option in [
756 serde_json::json!({ "type": "markdown", "addressable": false, "fulltext": true }),
757 serde_json::json!({ "type": "markdown", "addressable": false, "cypher": true }),
758 serde_json::json!({ "type": "markdown", "addressable": false, "default": "x" }),
759 ] {
760 assert!(
761 docs_schema_with(option.clone()).is_err(),
762 "{option} has to stay refused on a non-addressable field too"
763 );
764 }
765 }
766
767 #[test]
768 fn opting_out_survives_the_schema_document_it_is_rendered_into() {
769 let schema = docs_schema_with(serde_json::json!({ "type": "markdown", "addressable": false })).unwrap();
770
771 let again = parse_schema(&super::super::serialize::schema_document(&schema)).unwrap();
772
773 assert_eq!(again, schema);
774 assert!(!again.fields["page"].addressable);
775 }
776}