1use std::collections::BTreeMap;
11use std::fmt::Write as _;
12use std::sync::OnceLock;
13
14use regex::Regex;
15use serde::{Deserialize, Serialize};
16
17use crate::error::{Result, SurqlError};
18use crate::types::check_reserved_word;
19
20fn field_name_part_regex() -> &'static Regex {
21 static RE: OnceLock<Regex> = OnceLock::new();
22 RE.get_or_init(|| Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*$").expect("valid regex"))
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum FieldType {
29 String,
31 Int,
33 Float,
35 Bool,
37 Datetime,
39 Duration,
41 Decimal,
43 Number,
45 Object,
47 Array,
49 Record,
51 Geometry,
53 Any,
55}
56
57impl FieldType {
58 pub fn as_str(self) -> &'static str {
60 match self {
61 Self::String => "string",
62 Self::Int => "int",
63 Self::Float => "float",
64 Self::Bool => "bool",
65 Self::Datetime => "datetime",
66 Self::Duration => "duration",
67 Self::Decimal => "decimal",
68 Self::Number => "number",
69 Self::Object => "object",
70 Self::Array => "array",
71 Self::Record => "record",
72 Self::Geometry => "geometry",
73 Self::Any => "any",
74 }
75 }
76}
77
78impl std::fmt::Display for FieldType {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 f.write_str(self.as_str())
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98pub struct FieldDefinition {
99 pub name: String,
101 #[serde(rename = "type")]
103 pub field_type: FieldType,
104 #[serde(skip_serializing_if = "Option::is_none", default)]
106 pub assertion: Option<String>,
107 #[serde(skip_serializing_if = "Option::is_none", default)]
109 pub default: Option<String>,
110 #[serde(skip_serializing_if = "Option::is_none", default)]
112 pub value: Option<String>,
113 #[serde(skip_serializing_if = "Option::is_none", default)]
115 pub permissions: Option<BTreeMap<String, String>>,
116 #[serde(default)]
118 pub readonly: bool,
119 #[serde(default)]
121 pub flexible: bool,
122}
123
124impl FieldDefinition {
125 pub fn new(name: impl Into<String>, field_type: FieldType) -> Self {
130 Self {
131 name: name.into(),
132 field_type,
133 assertion: None,
134 default: None,
135 value: None,
136 permissions: None,
137 readonly: false,
138 flexible: false,
139 }
140 }
141
142 pub fn with_assertion(mut self, assertion: impl Into<String>) -> Self {
144 self.assertion = Some(assertion.into());
145 self
146 }
147
148 pub fn with_default(mut self, default: impl Into<String>) -> Self {
150 self.default = Some(default.into());
151 self
152 }
153
154 pub fn with_value(mut self, value: impl Into<String>) -> Self {
156 self.value = Some(value.into());
157 self
158 }
159
160 pub fn with_permissions<I, K, V>(mut self, permissions: I) -> Self
162 where
163 I: IntoIterator<Item = (K, V)>,
164 K: Into<String>,
165 V: Into<String>,
166 {
167 self.permissions = Some(
168 permissions
169 .into_iter()
170 .map(|(k, v)| (k.into(), v.into()))
171 .collect(),
172 );
173 self
174 }
175
176 pub fn readonly(mut self, readonly: bool) -> Self {
178 self.readonly = readonly;
179 self
180 }
181
182 pub fn flexible(mut self, flexible: bool) -> Self {
184 self.flexible = flexible;
185 self
186 }
187
188 pub fn validate(&self) -> Result<()> {
193 validate_field_name(&self.name)
194 }
195
196 pub fn to_surql(&self, table: &str) -> String {
211 self.to_surql_with_options(table, false)
212 }
213
214 pub fn to_surql_with_options(&self, table: &str, if_not_exists: bool) -> String {
216 let ine = if if_not_exists { " IF NOT EXISTS" } else { "" };
217 let mut sql = format!(
218 "DEFINE FIELD{ine} {name} ON TABLE {table} TYPE {ty}",
219 ine = ine,
220 name = self.name,
221 table = table,
222 ty = self.field_type.as_str(),
223 );
224 if let Some(assertion) = &self.assertion {
225 write!(sql, " ASSERT {}", assertion).expect("writing to String cannot fail");
226 }
227 if let Some(default) = &self.default {
228 write!(sql, " DEFAULT {}", default).expect("writing to String cannot fail");
229 }
230 if let Some(value) = &self.value {
231 write!(sql, " VALUE {}", value).expect("writing to String cannot fail");
232 }
233 if self.readonly {
234 sql.push_str(" READONLY");
235 }
236 if self.flexible {
237 sql.push_str(" FLEXIBLE");
238 }
239 sql.push(';');
240 sql
241 }
242}
243
244pub fn validate_field_name(name: &str) -> Result<()> {
260 if name.is_empty() {
261 return Err(SurqlError::Validation {
262 reason: "Field name cannot be empty".into(),
263 });
264 }
265 let regex = field_name_part_regex();
266 for part in name.split('.') {
267 if part.is_empty() {
268 return Err(SurqlError::Validation {
269 reason: format!("Invalid field name {name:?}: empty segment"),
270 });
271 }
272 if !regex.is_match(part) {
273 return Err(SurqlError::Validation {
274 reason: format!(
275 "Invalid field name {name:?}: segment {part:?} must contain only \
276 alphanumeric characters and underscores, and cannot start with a digit"
277 ),
278 });
279 }
280 }
281 Ok(())
282}
283
284pub fn field(name: impl Into<String>, field_type: FieldType) -> FieldBuilder {
301 FieldBuilder::new(name.into(), field_type)
302}
303
304#[derive(Debug, Clone)]
306pub struct FieldBuilder {
307 inner: FieldDefinition,
308}
309
310impl FieldBuilder {
311 fn new(name: String, field_type: FieldType) -> Self {
312 Self {
313 inner: FieldDefinition::new(name, field_type),
314 }
315 }
316
317 pub fn assertion(mut self, assertion: impl Into<String>) -> Self {
319 self.inner.assertion = Some(assertion.into());
320 self
321 }
322
323 pub fn default(mut self, default: impl Into<String>) -> Self {
325 self.inner.default = Some(default.into());
326 self
327 }
328
329 pub fn value(mut self, value: impl Into<String>) -> Self {
331 self.inner.value = Some(value.into());
332 self
333 }
334
335 pub fn permissions<I, K, V>(mut self, permissions: I) -> Self
337 where
338 I: IntoIterator<Item = (K, V)>,
339 K: Into<String>,
340 V: Into<String>,
341 {
342 self.inner.permissions = Some(
343 permissions
344 .into_iter()
345 .map(|(k, v)| (k.into(), v.into()))
346 .collect(),
347 );
348 self
349 }
350
351 pub fn readonly(mut self, readonly: bool) -> Self {
353 self.inner.readonly = readonly;
354 self
355 }
356
357 pub fn flexible(mut self, flexible: bool) -> Self {
359 self.inner.flexible = flexible;
360 self
361 }
362
363 pub fn build(self) -> Result<(FieldDefinition, Option<String>)> {
366 self.inner.validate()?;
367 let warning = check_reserved_word(&self.inner.name, false);
368 Ok((self.inner, warning))
369 }
370
371 pub fn build_unchecked(self) -> Result<FieldDefinition> {
373 self.inner.validate()?;
374 Ok(self.inner)
375 }
376}
377
378pub fn string_field(name: impl Into<String>) -> FieldBuilder {
380 field(name, FieldType::String)
381}
382
383pub fn int_field(name: impl Into<String>) -> FieldBuilder {
385 field(name, FieldType::Int)
386}
387
388pub fn float_field(name: impl Into<String>) -> FieldBuilder {
390 field(name, FieldType::Float)
391}
392
393pub fn bool_field(name: impl Into<String>) -> FieldBuilder {
395 field(name, FieldType::Bool)
396}
397
398pub fn datetime_field(name: impl Into<String>) -> FieldBuilder {
400 field(name, FieldType::Datetime)
401}
402
403pub fn array_field(name: impl Into<String>) -> FieldBuilder {
405 field(name, FieldType::Array)
406}
407
408pub fn object_field(name: impl Into<String>) -> FieldBuilder {
412 field(name, FieldType::Object).flexible(true)
413}
414
415pub fn record_field(name: impl Into<String>, table: Option<&str>) -> FieldBuilder {
421 let mut builder = field(name, FieldType::Record);
422 if let Some(target) = table {
423 builder.inner.assertion = Some(format!("$value.table = \"{target}\""));
424 }
425 builder
426}
427
428pub fn computed_field(
433 name: impl Into<String>,
434 value: impl Into<String>,
435 field_type: FieldType,
436) -> FieldBuilder {
437 field(name, field_type).value(value).readonly(true)
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn field_type_as_str_matches_lowercase() {
446 assert_eq!(FieldType::String.as_str(), "string");
447 assert_eq!(FieldType::Datetime.as_str(), "datetime");
448 assert_eq!(FieldType::Any.as_str(), "any");
449 }
450
451 #[test]
452 fn field_type_display_matches_as_str() {
453 assert_eq!(format!("{}", FieldType::Int), "int");
454 }
455
456 #[test]
457 fn field_type_serializes_lowercase() {
458 let json = serde_json::to_string(&FieldType::Datetime).unwrap();
459 assert_eq!(json, "\"datetime\"");
460 }
461
462 #[test]
463 fn field_type_deserializes_lowercase() {
464 let ft: FieldType = serde_json::from_str("\"bool\"").unwrap();
465 assert_eq!(ft, FieldType::Bool);
466 }
467
468 #[test]
469 fn new_sets_defaults() {
470 let f = FieldDefinition::new("email", FieldType::String);
471 assert_eq!(f.name, "email");
472 assert_eq!(f.field_type, FieldType::String);
473 assert!(f.assertion.is_none());
474 assert!(!f.readonly);
475 assert!(!f.flexible);
476 }
477
478 #[test]
479 fn to_surql_minimal() {
480 let f = FieldDefinition::new("email", FieldType::String);
481 assert_eq!(
482 f.to_surql("user"),
483 "DEFINE FIELD email ON TABLE user TYPE string;"
484 );
485 }
486
487 #[test]
488 fn to_surql_with_assertion() {
489 let f = FieldDefinition::new("email", FieldType::String)
490 .with_assertion("string::is::email($value)");
491 assert_eq!(
492 f.to_surql("user"),
493 "DEFINE FIELD email ON TABLE user TYPE string ASSERT string::is::email($value);"
494 );
495 }
496
497 #[test]
498 fn to_surql_with_default() {
499 let f = FieldDefinition::new("created_at", FieldType::Datetime).with_default("time::now()");
500 assert_eq!(
501 f.to_surql("event"),
502 "DEFINE FIELD created_at ON TABLE event TYPE datetime DEFAULT time::now();"
503 );
504 }
505
506 #[test]
507 fn to_surql_readonly_flexible() {
508 let f = FieldDefinition::new("meta", FieldType::Object)
509 .readonly(true)
510 .flexible(true);
511 assert_eq!(
512 f.to_surql("user"),
513 "DEFINE FIELD meta ON TABLE user TYPE object READONLY FLEXIBLE;"
514 );
515 }
516
517 #[test]
518 fn to_surql_with_value_expression() {
519 let f = FieldDefinition::new("full", FieldType::String).with_value("string::concat(a,b)");
520 assert!(f.to_surql("t").contains("VALUE string::concat(a,b)"));
521 }
522
523 #[test]
524 fn to_surql_if_not_exists() {
525 let f = FieldDefinition::new("name", FieldType::String);
526 assert_eq!(
527 f.to_surql_with_options("user", true),
528 "DEFINE FIELD IF NOT EXISTS name ON TABLE user TYPE string;"
529 );
530 }
531
532 #[test]
533 fn validate_rejects_empty_name() {
534 let f = FieldDefinition::new("", FieldType::String);
535 assert!(f.validate().is_err());
536 }
537
538 #[test]
539 fn validate_rejects_bad_leading_digit() {
540 let f = FieldDefinition::new("1bad", FieldType::String);
541 assert!(f.validate().is_err());
542 }
543
544 #[test]
545 fn validate_allows_dot_nested() {
546 let f = FieldDefinition::new("address.city", FieldType::String);
547 assert!(f.validate().is_ok());
548 }
549
550 #[test]
551 fn validate_rejects_empty_segment() {
552 let f = FieldDefinition::new("address..city", FieldType::String);
553 assert!(f.validate().is_err());
554 }
555
556 #[test]
557 fn builder_string_field() {
558 let (f, _) = string_field("email").build().unwrap();
559 assert_eq!(f.field_type, FieldType::String);
560 }
561
562 #[test]
563 fn builder_int_field_with_assertion() {
564 let (f, _) = int_field("age").assertion("$value >= 0").build().unwrap();
565 assert_eq!(f.field_type, FieldType::Int);
566 assert_eq!(f.assertion.as_deref(), Some("$value >= 0"));
567 }
568
569 #[test]
570 fn builder_float_field() {
571 let (f, _) = float_field("price").build().unwrap();
572 assert_eq!(f.field_type, FieldType::Float);
573 }
574
575 #[test]
576 fn builder_bool_field_with_default() {
577 let (f, _) = bool_field("active").default("true").build().unwrap();
578 assert_eq!(f.field_type, FieldType::Bool);
579 assert_eq!(f.default.as_deref(), Some("true"));
580 }
581
582 #[test]
583 fn builder_datetime_field_readonly() {
584 let (f, _) = datetime_field("created_at")
585 .default("time::now()")
586 .readonly(true)
587 .build()
588 .unwrap();
589 assert!(f.readonly);
590 assert_eq!(f.default.as_deref(), Some("time::now()"));
591 }
592
593 #[test]
594 fn builder_array_field() {
595 let (f, _) = array_field("tags").default("[]").build().unwrap();
596 assert_eq!(f.field_type, FieldType::Array);
597 }
598
599 #[test]
600 fn builder_object_field_defaults_flexible() {
601 let (f, _) = object_field("metadata").build().unwrap();
602 assert_eq!(f.field_type, FieldType::Object);
603 assert!(f.flexible);
604 }
605
606 #[test]
607 fn builder_record_field_with_table() {
608 let (f, _) = record_field("author", Some("user")).build().unwrap();
609 assert_eq!(f.field_type, FieldType::Record);
610 assert_eq!(f.assertion.as_deref(), Some(r#"$value.table = "user""#),);
611 }
612
613 #[test]
614 fn builder_record_field_no_table() {
615 let (f, _) = record_field("link", None).build().unwrap();
616 assert!(f.assertion.is_none());
617 }
618
619 #[test]
620 fn builder_computed_field_is_readonly() {
621 let (f, _) = computed_field("full", "a + b", FieldType::String)
622 .build()
623 .unwrap();
624 assert!(f.readonly);
625 assert_eq!(f.value.as_deref(), Some("a + b"));
626 }
627
628 #[test]
629 fn builder_rejects_invalid_name() {
630 let err = string_field("1bad").build().unwrap_err();
631 assert!(matches!(err, SurqlError::Validation { .. }));
632 }
633
634 #[test]
635 fn builder_flags_reserved_word() {
636 let (_f, warning) = string_field("select").build().unwrap();
637 assert!(warning.is_some());
638 }
639
640 #[test]
641 fn builder_permissions_are_stored() {
642 let (f, _) = string_field("name")
643 .permissions([("select", "true")])
644 .build()
645 .unwrap();
646 assert_eq!(
647 f.permissions
648 .as_ref()
649 .unwrap()
650 .get("select")
651 .map(String::as_str),
652 Some("true")
653 );
654 }
655
656 #[test]
657 fn validate_field_name_helper() {
658 assert!(validate_field_name("ok").is_ok());
659 assert!(validate_field_name("ok.nested").is_ok());
660 assert!(validate_field_name("").is_err());
661 assert!(validate_field_name("bad seg").is_err());
662 }
663}