1use anyhow::{bail, Result};
2use dialoguer::{Confirm, Input, Select};
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
6pub enum RelationType {
7 BelongsTo,
8 HasMany,
9 ManyToMany,
10}
11
12#[derive(Debug, Clone, Serialize)]
13pub struct RelationDefinition {
14 pub name: String,
15 pub relation_type: RelationType,
16 pub target_entity: String,
17 pub fk_column: Option<String>,
18 pub optional: bool,
19}
20
21#[derive(Debug, Clone, Serialize)]
22pub struct EntityDefinition {
23 pub name: String,
24 pub fields: Vec<FieldDefinition>,
25 pub relations: Vec<RelationDefinition>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
29pub enum ValidationRule {
30 Min(u64),
31 Max(u64),
32 Email,
33 Url,
34 Regex(String),
35 Required,
36 Unique,
37}
38
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub enum FieldVisibility {
41 Public, Authenticated, AdminOnly, Roles(Vec<String>), }
46
47impl Default for FieldVisibility {
48 fn default() -> Self {
49 FieldVisibility::Public
50 }
51}
52
53#[derive(Debug, Clone, Serialize)]
54pub struct FieldDefinition {
55 pub name: String,
56 pub field_type: FieldType,
57 pub optional: bool,
58 pub default: Option<String>,
59 pub relation: Option<String>,
60 #[serde(default)]
61 pub validations: Vec<ValidationRule>,
62 #[serde(default)]
63 pub searchable: bool,
64 #[serde(default)]
65 pub visibility: FieldVisibility,
66}
67
68#[derive(Debug, Clone, Serialize)]
69pub enum FieldType {
70 String,
71 Text,
72 Bool,
73 Int32,
74 Int64,
75 Float64,
76 Decimal,
77 Uuid,
78 DateTime,
79 Date,
80 Json,
81 Enum(Vec<String>),
82 File,
83 Image,
84}
85
86impl FieldType {
87 pub fn to_rust(&self) -> &str {
88 match self {
89 FieldType::String => "String",
90 FieldType::Text => "String",
91 FieldType::Bool => "bool",
92 FieldType::Int32 => "i32",
93 FieldType::Int64 => "i64",
94 FieldType::Float64 => "f64",
95 FieldType::Decimal => "Decimal",
96 FieldType::Uuid => "Uuid",
97 FieldType::DateTime => "DateTimeWithTimeZone",
98 FieldType::Date => "Date",
99 FieldType::Json => "Json",
100 FieldType::Enum(_) => "String",
101 FieldType::File => "String",
102 FieldType::Image => "String",
103 }
104 }
105
106 pub fn to_typescript(&self) -> &str {
107 match self {
108 FieldType::String
109 | FieldType::Text
110 | FieldType::Uuid
111 | FieldType::Enum(_)
112 | FieldType::File
113 | FieldType::Image => "string",
114 FieldType::Bool => "boolean",
115 FieldType::Int32 | FieldType::Int64 | FieldType::Float64 | FieldType::Decimal => {
116 "number"
117 }
118 FieldType::DateTime | FieldType::Date => "string",
119 FieldType::Json => "unknown",
120 }
121 }
122
123 pub fn to_postgres(&self) -> &str {
124 match self {
125 FieldType::String => "VARCHAR(255)",
126 FieldType::Text => "TEXT",
127 FieldType::Bool => "BOOLEAN",
128 FieldType::Int32 => "INTEGER",
129 FieldType::Int64 => "BIGINT",
130 FieldType::Float64 => "DOUBLE PRECISION",
131 FieldType::Decimal => "DECIMAL",
132 FieldType::Uuid => "UUID",
133 FieldType::DateTime => "TIMESTAMPTZ",
134 FieldType::Date => "DATE",
135 FieldType::Json => "JSONB",
136 FieldType::Enum(_) => "VARCHAR(255)",
137 FieldType::File => "VARCHAR(512)",
138 FieldType::Image => "VARCHAR(512)",
139 }
140 }
141
142 pub fn to_sea_orm_column(&self) -> &str {
143 match self {
144 FieldType::String => "ColumnType::String(StringLen::N(255))",
145 FieldType::Text => "ColumnType::Text",
146 FieldType::Bool => "ColumnType::Boolean",
147 FieldType::Int32 => "ColumnType::Integer",
148 FieldType::Int64 => "ColumnType::BigInteger",
149 FieldType::Float64 => "ColumnType::Double",
150 FieldType::Decimal => "ColumnType::Decimal(None)",
151 FieldType::Uuid => "ColumnType::Uuid",
152 FieldType::DateTime => "ColumnType::TimestampWithTimeZone",
153 FieldType::Date => "ColumnType::Date",
154 FieldType::Json => "ColumnType::JsonBinary",
155 FieldType::Enum(_) => "ColumnType::String(StringLen::N(255))",
156 FieldType::File => "ColumnType::String(StringLen::N(512))",
157 FieldType::Image => "ColumnType::String(StringLen::N(512))",
158 }
159 }
160
161 pub fn to_sea_orm_migration(&self) -> &str {
163 match self {
164 FieldType::String => "string_len(255)",
165 FieldType::Text => "text()",
166 FieldType::Bool => "boolean()",
167 FieldType::Int32 => "integer()",
168 FieldType::Int64 => "big_integer()",
169 FieldType::Float64 => "double()",
170 FieldType::Decimal => "decimal()",
171 FieldType::Uuid => "uuid()",
172 FieldType::DateTime => "timestamp_with_time_zone()",
173 FieldType::Date => "date()",
174 FieldType::Json => "json_binary()",
175 FieldType::Enum(_) => "string_len(255)",
176 FieldType::File => "string_len(512)",
177 FieldType::Image => "string_len(512)",
178 }
179 }
180
181 pub fn to_shadcn(&self) -> &str {
182 match self {
183 FieldType::String | FieldType::Uuid => "Input",
184 FieldType::Text => "Textarea",
185 FieldType::Bool => "Switch",
186 FieldType::Int32 | FieldType::Int64 | FieldType::Float64 | FieldType::Decimal => {
187 "Input"
188 }
189 FieldType::DateTime | FieldType::Date => "Input",
190 FieldType::Json => "Textarea",
191 FieldType::Enum(_) => "Select",
192 FieldType::File => "FileInput",
193 FieldType::Image => "ImageInput",
194 }
195 }
196
197 pub fn input_type(&self) -> &str {
198 match self {
199 FieldType::Int32 | FieldType::Int64 | FieldType::Float64 | FieldType::Decimal => {
200 "number"
201 }
202 FieldType::Date => "date",
203 FieldType::DateTime => "datetime-local",
204 FieldType::File | FieldType::Image => "file",
205 _ => "text",
206 }
207 }
208}
209
210fn parse_field_type(s: &str) -> Result<FieldType> {
211 let lower = s.to_lowercase();
212 if lower.starts_with("enum(") && lower.ends_with(')') {
213 let inner = &s[5..s.len() - 1];
214 let variants: Vec<String> = inner
215 .split(',')
216 .map(|v| v.trim().to_string())
217 .filter(|v| !v.is_empty())
218 .collect();
219 if variants.is_empty() {
220 bail!("Enum type requires at least one variant: enum(a,b,c)");
221 }
222 return Ok(FieldType::Enum(variants));
223 }
224 match lower.as_str() {
225 "string" | "str" => Ok(FieldType::String),
226 "text" => Ok(FieldType::Text),
227 "bool" | "boolean" => Ok(FieldType::Bool),
228 "i32" | "int" | "int32" | "integer" => Ok(FieldType::Int32),
229 "i64" | "int64" | "bigint" => Ok(FieldType::Int64),
230 "f64" | "float" | "float64" | "double" => Ok(FieldType::Float64),
231 "decimal" | "money" => Ok(FieldType::Decimal),
232 "uuid" => Ok(FieldType::Uuid),
233 "datetime" | "timestamp" => Ok(FieldType::DateTime),
234 "date" => Ok(FieldType::Date),
235 "json" | "jsonb" => Ok(FieldType::Json),
236 "file" => Ok(FieldType::File),
237 "image" => Ok(FieldType::Image),
238 _ => bail!("Unknown field type: '{}'", s),
239 }
240}
241
242fn parse_validations(s: &str) -> Result<Vec<ValidationRule>> {
248 let mut rules = Vec::new();
249
250 for part in s.split(',') {
251 let part = part.trim();
252 if part.is_empty() {
253 continue;
254 }
255
256 if let Some((key, value)) = part.split_once('=') {
257 match key.trim() {
258 "min" => {
259 let n = value.trim().parse::<u64>()
260 .map_err(|_| anyhow::anyhow!("Invalid min value '{}': expected a positive integer", value.trim()))?;
261 rules.push(ValidationRule::Min(n));
262 }
263 "max" => {
264 let n = value.trim().parse::<u64>()
265 .map_err(|_| anyhow::anyhow!("Invalid max value '{}': expected a positive integer", value.trim()))?;
266 rules.push(ValidationRule::Max(n));
267 }
268 "regex" => {
269 let pattern = value.trim().to_string();
270 regex::Regex::new(&pattern)
271 .map_err(|e| anyhow::anyhow!("Invalid regex pattern '{}': {}", pattern, e))?;
272 rules.push(ValidationRule::Regex(pattern));
273 }
274 "roles" => {} _ => {}
276 }
277 } else {
278 match part {
279 "email" => rules.push(ValidationRule::Email),
280 "url" => rules.push(ValidationRule::Url),
281 "required" => rules.push(ValidationRule::Required),
282 "unique" => rules.push(ValidationRule::Unique),
283 "searchable" => {} "admin_only" => {} "authenticated" => {} _ => {}
287 }
288 }
289 }
290
291 Ok(rules)
292}
293
294fn parse_visibility(s: &str) -> FieldVisibility {
302 for part in s.split(',') {
303 let part = part.trim();
304 if part.is_empty() {
305 continue;
306 }
307
308 if part == "admin_only" {
309 return FieldVisibility::AdminOnly;
310 }
311 if part == "authenticated" {
312 return FieldVisibility::Authenticated;
313 }
314
315 if let Some((key, value)) = part.split_once('=') {
316 if key.trim() == "roles" {
317 let roles: Vec<String> = value
318 .split(';')
319 .map(|r| r.trim().to_string())
320 .filter(|r| !r.is_empty())
321 .collect();
322 if !roles.is_empty() {
323 return FieldVisibility::Roles(roles);
324 }
325 }
326 }
327 }
328
329 FieldVisibility::Public
330}
331
332pub fn parse_entity(name: &str, field_strs: &[String]) -> Result<EntityDefinition> {
338 let mut fields = Vec::new();
339 let mut relations = Vec::new();
340
341 for field_str in field_strs {
342 let (field_str, optional) = if field_str.ends_with('?') {
343 (&field_str[..field_str.len() - 1], true)
344 } else {
345 (field_str.as_str(), false)
346 };
347
348 let parts: Vec<&str> = field_str.splitn(2, ':').collect();
349 if parts.len() != 2 {
350 bail!(
351 "Invalid field format '{}'. Expected name:type",
352 field_str
353 );
354 }
355
356 let field_name = parts[0].to_string();
357
358 let (type_and_relation_str, annotations) = if let Some(bracket_start) = parts[1].find('[') {
360 if let Some(bracket_end) = parts[1].find(']') {
361 let annotations_str = &parts[1][bracket_start + 1..bracket_end];
362 let type_str = &parts[1][..bracket_start];
363 let after_bracket = &parts[1][bracket_end + 1..];
365 let full_type = format!("{}{}", type_str, after_bracket);
366 (full_type, annotations_str.to_string())
367 } else {
368 (parts[1].to_string(), String::new())
369 }
370 } else {
371 (parts[1].to_string(), String::new())
372 };
373
374 let type_and_relation: Vec<&str> = type_and_relation_str.splitn(2, "->").collect();
375 let raw_type_str = type_and_relation[0];
377 let (type_str, type_optional) = if raw_type_str.ends_with('?') {
378 (raw_type_str[..raw_type_str.len() - 1].to_lowercase(), true)
379 } else {
380 (raw_type_str.to_lowercase(), false)
381 };
382 let optional = optional || type_optional;
383
384 match type_str.as_str() {
386 "has_many" => {
387 let target = type_and_relation
388 .get(1)
389 .ok_or_else(|| anyhow::anyhow!("has_many requires target entity: {}:has_many->Entity", field_name))?;
390 relations.push(RelationDefinition {
391 name: field_name,
392 relation_type: RelationType::HasMany,
393 target_entity: target.to_string(),
394 fk_column: None,
395 optional: false,
396 });
397 continue;
398 }
399 "m2m" => {
400 let target = type_and_relation
401 .get(1)
402 .ok_or_else(|| anyhow::anyhow!("m2m requires target entity: {}:m2m->Entity", field_name))?;
403 relations.push(RelationDefinition {
404 name: field_name,
405 relation_type: RelationType::ManyToMany,
406 target_entity: target.to_string(),
407 fk_column: None,
408 optional: false,
409 });
410 continue;
411 }
412 _ => {}
413 }
414
415 let field_type = parse_field_type(&type_str)?;
416 let relation = type_and_relation.get(1).map(|s| s.to_string());
417
418 let validations = parse_validations(&annotations)?;
420 let searchable = annotations.contains("searchable");
421 let visibility = parse_visibility(&annotations);
422
423 if let Some(ref target) = relation {
425 relations.push(RelationDefinition {
426 name: field_name.clone(),
427 relation_type: RelationType::BelongsTo,
428 target_entity: target.clone(),
429 fk_column: Some(field_name.clone()),
430 optional,
431 });
432 }
433
434 fields.push(FieldDefinition {
435 name: field_name,
436 field_type,
437 optional,
438 default: None,
439 relation,
440 validations,
441 searchable,
442 visibility,
443 });
444 }
445
446 Ok(EntityDefinition {
447 name: name.to_string(),
448 fields,
449 relations,
450 })
451}
452
453const FIELD_TYPE_OPTIONS: &[(&str, &str)] = &[
454 ("string", "String (VARCHAR 255)"),
455 ("text", "Text (unlimited)"),
456 ("bool", "Boolean"),
457 ("int", "Integer (i32)"),
458 ("bigint", "Big Integer (i64)"),
459 ("float", "Float (f64)"),
460 ("decimal", "Decimal"),
461 ("uuid", "UUID"),
462 ("datetime", "DateTime (with timezone)"),
463 ("date", "Date"),
464 ("json", "JSON"),
465 ("file", "File (upload)"),
466 ("image", "Image (upload with validation)"),
467 ("enum", "Enum (custom variants)"),
468];
469
470const RELATION_TYPE_OPTIONS: &[(&str, &str)] = &[
471 ("belongs_to", "Belongs To (FK on this entity)"),
472 ("has_many", "Has Many (reverse side)"),
473 ("m2m", "Many to Many (junction table)"),
474];
475
476pub fn prompt_entity_fields(entity_name: &str) -> Result<(Vec<FieldDefinition>, Vec<RelationDefinition>)> {
478 println!(
479 "Define fields for '{}' (press Enter with empty name to finish):",
480 entity_name
481 );
482
483 let type_labels: Vec<&str> = FIELD_TYPE_OPTIONS.iter().map(|(_, label)| *label).collect();
484 let mut fields = Vec::new();
485 let mut relations = Vec::new();
486
487 loop {
488 let field_name: String = Input::new()
489 .with_prompt("Field name")
490 .allow_empty(true)
491 .interact_text()?;
492
493 if field_name.is_empty() {
494 break;
495 }
496
497 let type_idx = Select::new()
498 .with_prompt("Field type")
499 .items(&type_labels)
500 .default(0)
501 .interact()?;
502
503 let optional = Confirm::new()
504 .with_prompt("Optional (nullable)?")
505 .default(false)
506 .interact()?;
507
508 let relation: String = Input::new()
509 .with_prompt("Foreign key (entity name, or empty to skip)")
510 .allow_empty(true)
511 .interact_text()?;
512
513 let (type_key, _) = FIELD_TYPE_OPTIONS[type_idx];
514 let field_type = if type_key == "enum" {
515 let variants_input: String = Input::new()
516 .with_prompt("Enter enum variants (comma-separated)")
517 .interact_text()?;
518 let variants: Vec<String> = variants_input
519 .split(',')
520 .map(|v| v.trim().to_string())
521 .filter(|v| !v.is_empty())
522 .collect();
523 if variants.is_empty() {
524 println!("No variants provided, defaulting to String type.");
525 FieldType::String
526 } else {
527 FieldType::Enum(variants)
528 }
529 } else {
530 parse_field_type(type_key)?
531 };
532 let relation = if relation.is_empty() {
533 None
534 } else {
535 Some(relation)
536 };
537
538 if let Some(ref target) = relation {
539 relations.push(RelationDefinition {
540 name: field_name.clone(),
541 relation_type: RelationType::BelongsTo,
542 target_entity: target.clone(),
543 fk_column: Some(field_name.clone()),
544 optional,
545 });
546 }
547
548 fields.push(FieldDefinition {
549 name: field_name,
550 field_type,
551 optional,
552 default: None,
553 relation,
554 validations: Vec::new(),
555 searchable: false,
556 visibility: FieldVisibility::default(),
557 });
558
559 println!();
560 }
561
562 let add_relations = Confirm::new()
564 .with_prompt("Add relations (has_many, m2m)?")
565 .default(false)
566 .interact()?;
567
568 if add_relations {
569 let rel_labels: Vec<&str> = RELATION_TYPE_OPTIONS.iter().map(|(_, label)| *label).collect();
570
571 loop {
572 let rel_name: String = Input::new()
573 .with_prompt("Relation name (empty to finish)")
574 .allow_empty(true)
575 .interact_text()?;
576
577 if rel_name.is_empty() {
578 break;
579 }
580
581 let rel_idx = Select::new()
582 .with_prompt("Relation type")
583 .items(&rel_labels)
584 .default(0)
585 .interact()?;
586
587 let target: String = Input::new()
588 .with_prompt("Target entity (PascalCase)")
589 .interact_text()?;
590
591 let (rel_key, _) = RELATION_TYPE_OPTIONS[rel_idx];
592 let relation_type = match rel_key {
593 "belongs_to" => RelationType::BelongsTo,
594 "has_many" => RelationType::HasMany,
595 "m2m" => RelationType::ManyToMany,
596 _ => unreachable!(),
597 };
598
599 relations.push(RelationDefinition {
600 name: rel_name,
601 relation_type,
602 target_entity: target,
603 fk_column: None,
604 optional: false,
605 });
606
607 println!();
608 }
609 }
610
611 Ok((fields, relations))
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617
618 #[test]
621 fn parse_string_aliases() {
622 assert!(matches!(parse_field_type("string").unwrap(), FieldType::String));
623 assert!(matches!(parse_field_type("str").unwrap(), FieldType::String));
624 assert!(matches!(parse_field_type("STRING").unwrap(), FieldType::String));
625 }
626
627 #[test]
628 fn parse_text() {
629 assert!(matches!(parse_field_type("text").unwrap(), FieldType::Text));
630 }
631
632 #[test]
633 fn parse_bool_aliases() {
634 assert!(matches!(parse_field_type("bool").unwrap(), FieldType::Bool));
635 assert!(matches!(parse_field_type("boolean").unwrap(), FieldType::Bool));
636 }
637
638 #[test]
639 fn parse_int32_aliases() {
640 assert!(matches!(parse_field_type("i32").unwrap(), FieldType::Int32));
641 assert!(matches!(parse_field_type("int").unwrap(), FieldType::Int32));
642 assert!(matches!(parse_field_type("int32").unwrap(), FieldType::Int32));
643 assert!(matches!(parse_field_type("integer").unwrap(), FieldType::Int32));
644 }
645
646 #[test]
647 fn parse_int64_aliases() {
648 assert!(matches!(parse_field_type("i64").unwrap(), FieldType::Int64));
649 assert!(matches!(parse_field_type("int64").unwrap(), FieldType::Int64));
650 assert!(matches!(parse_field_type("bigint").unwrap(), FieldType::Int64));
651 }
652
653 #[test]
654 fn parse_float64_aliases() {
655 assert!(matches!(parse_field_type("f64").unwrap(), FieldType::Float64));
656 assert!(matches!(parse_field_type("float").unwrap(), FieldType::Float64));
657 assert!(matches!(parse_field_type("float64").unwrap(), FieldType::Float64));
658 assert!(matches!(parse_field_type("double").unwrap(), FieldType::Float64));
659 }
660
661 #[test]
662 fn parse_decimal_aliases() {
663 assert!(matches!(parse_field_type("decimal").unwrap(), FieldType::Decimal));
664 assert!(matches!(parse_field_type("money").unwrap(), FieldType::Decimal));
665 }
666
667 #[test]
668 fn parse_uuid() {
669 assert!(matches!(parse_field_type("uuid").unwrap(), FieldType::Uuid));
670 }
671
672 #[test]
673 fn parse_datetime_aliases() {
674 assert!(matches!(parse_field_type("datetime").unwrap(), FieldType::DateTime));
675 assert!(matches!(parse_field_type("timestamp").unwrap(), FieldType::DateTime));
676 }
677
678 #[test]
679 fn parse_date() {
680 assert!(matches!(parse_field_type("date").unwrap(), FieldType::Date));
681 }
682
683 #[test]
684 fn parse_json_aliases() {
685 assert!(matches!(parse_field_type("json").unwrap(), FieldType::Json));
686 assert!(matches!(parse_field_type("jsonb").unwrap(), FieldType::Json));
687 }
688
689 #[test]
690 fn parse_file() {
691 assert!(matches!(parse_field_type("file").unwrap(), FieldType::File));
692 }
693
694 #[test]
695 fn parse_image() {
696 assert!(matches!(parse_field_type("image").unwrap(), FieldType::Image));
697 }
698
699 #[test]
700 fn parse_enum_type() {
701 let ft = parse_field_type("enum(draft,published,archived)").unwrap();
702 assert!(matches!(ft, FieldType::Enum(ref v) if v.len() == 3));
703 }
704
705 #[test]
706 fn parse_enum_type_with_spaces() {
707 let ft = parse_field_type("enum(a, b, c)").unwrap();
708 assert!(matches!(ft, FieldType::Enum(ref v) if v.len() == 3 && v[0] == "a"));
709 }
710
711 #[test]
712 fn parse_enum_empty_errors() {
713 assert!(parse_field_type("enum()").is_err());
714 }
715
716 #[test]
717 fn parse_enum_case_insensitive() {
718 let ft = parse_field_type("Enum(Draft,Published)").unwrap();
719 assert!(matches!(ft, FieldType::Enum(ref v) if v.len() == 2 && v[0] == "Draft"));
720 }
721
722 #[test]
723 fn parse_unknown_type_errors() {
724 assert!(parse_field_type("foobar").is_err());
725 }
726
727 #[test]
730 fn to_rust_mappings() {
731 assert_eq!(FieldType::String.to_rust(), "String");
732 assert_eq!(FieldType::Text.to_rust(), "String");
733 assert_eq!(FieldType::Bool.to_rust(), "bool");
734 assert_eq!(FieldType::Int32.to_rust(), "i32");
735 assert_eq!(FieldType::Int64.to_rust(), "i64");
736 assert_eq!(FieldType::Float64.to_rust(), "f64");
737 assert_eq!(FieldType::Decimal.to_rust(), "Decimal");
738 assert_eq!(FieldType::Uuid.to_rust(), "Uuid");
739 assert_eq!(FieldType::DateTime.to_rust(), "DateTimeWithTimeZone");
740 assert_eq!(FieldType::Date.to_rust(), "Date");
741 assert_eq!(FieldType::Json.to_rust(), "Json");
742 assert_eq!(FieldType::Enum(vec!["A".into()]).to_rust(), "String");
743 assert_eq!(FieldType::File.to_rust(), "String");
744 assert_eq!(FieldType::Image.to_rust(), "String");
745 }
746
747 #[test]
750 fn to_typescript_mappings() {
751 assert_eq!(FieldType::String.to_typescript(), "string");
752 assert_eq!(FieldType::Text.to_typescript(), "string");
753 assert_eq!(FieldType::Bool.to_typescript(), "boolean");
754 assert_eq!(FieldType::Int32.to_typescript(), "number");
755 assert_eq!(FieldType::Int64.to_typescript(), "number");
756 assert_eq!(FieldType::Float64.to_typescript(), "number");
757 assert_eq!(FieldType::Decimal.to_typescript(), "number");
758 assert_eq!(FieldType::Uuid.to_typescript(), "string");
759 assert_eq!(FieldType::DateTime.to_typescript(), "string");
760 assert_eq!(FieldType::Date.to_typescript(), "string");
761 assert_eq!(FieldType::Json.to_typescript(), "unknown");
762 assert_eq!(FieldType::Enum(vec![]).to_typescript(), "string");
763 assert_eq!(FieldType::File.to_typescript(), "string");
764 assert_eq!(FieldType::Image.to_typescript(), "string");
765 }
766
767 #[test]
770 fn to_postgres_mappings() {
771 assert_eq!(FieldType::String.to_postgres(), "VARCHAR(255)");
772 assert_eq!(FieldType::Text.to_postgres(), "TEXT");
773 assert_eq!(FieldType::Bool.to_postgres(), "BOOLEAN");
774 assert_eq!(FieldType::Int32.to_postgres(), "INTEGER");
775 assert_eq!(FieldType::Int64.to_postgres(), "BIGINT");
776 assert_eq!(FieldType::Float64.to_postgres(), "DOUBLE PRECISION");
777 assert_eq!(FieldType::Decimal.to_postgres(), "DECIMAL");
778 assert_eq!(FieldType::Uuid.to_postgres(), "UUID");
779 assert_eq!(FieldType::DateTime.to_postgres(), "TIMESTAMPTZ");
780 assert_eq!(FieldType::Date.to_postgres(), "DATE");
781 assert_eq!(FieldType::Json.to_postgres(), "JSONB");
782 assert_eq!(FieldType::File.to_postgres(), "VARCHAR(512)");
783 assert_eq!(FieldType::Image.to_postgres(), "VARCHAR(512)");
784 }
785
786 #[test]
789 fn to_sea_orm_column_mappings() {
790 assert_eq!(FieldType::String.to_sea_orm_column(), "ColumnType::String(StringLen::N(255))");
791 assert_eq!(FieldType::Text.to_sea_orm_column(), "ColumnType::Text");
792 assert_eq!(FieldType::Bool.to_sea_orm_column(), "ColumnType::Boolean");
793 assert_eq!(FieldType::Int32.to_sea_orm_column(), "ColumnType::Integer");
794 assert_eq!(FieldType::Int64.to_sea_orm_column(), "ColumnType::BigInteger");
795 assert_eq!(FieldType::Float64.to_sea_orm_column(), "ColumnType::Double");
796 assert_eq!(FieldType::Decimal.to_sea_orm_column(), "ColumnType::Decimal(None)");
797 assert_eq!(FieldType::Uuid.to_sea_orm_column(), "ColumnType::Uuid");
798 assert_eq!(FieldType::DateTime.to_sea_orm_column(), "ColumnType::TimestampWithTimeZone");
799 assert_eq!(FieldType::Date.to_sea_orm_column(), "ColumnType::Date");
800 assert_eq!(FieldType::Json.to_sea_orm_column(), "ColumnType::JsonBinary");
801 assert_eq!(FieldType::File.to_sea_orm_column(), "ColumnType::String(StringLen::N(512))");
802 assert_eq!(FieldType::Image.to_sea_orm_column(), "ColumnType::String(StringLen::N(512))");
803 }
804
805 #[test]
808 fn to_sea_orm_migration_mappings() {
809 assert_eq!(FieldType::String.to_sea_orm_migration(), "string_len(255)");
810 assert_eq!(FieldType::Text.to_sea_orm_migration(), "text()");
811 assert_eq!(FieldType::Bool.to_sea_orm_migration(), "boolean()");
812 assert_eq!(FieldType::Int32.to_sea_orm_migration(), "integer()");
813 assert_eq!(FieldType::Int64.to_sea_orm_migration(), "big_integer()");
814 assert_eq!(FieldType::Float64.to_sea_orm_migration(), "double()");
815 assert_eq!(FieldType::Decimal.to_sea_orm_migration(), "decimal()");
816 assert_eq!(FieldType::Uuid.to_sea_orm_migration(), "uuid()");
817 assert_eq!(FieldType::DateTime.to_sea_orm_migration(), "timestamp_with_time_zone()");
818 assert_eq!(FieldType::Date.to_sea_orm_migration(), "date()");
819 assert_eq!(FieldType::Json.to_sea_orm_migration(), "json_binary()");
820 assert_eq!(FieldType::File.to_sea_orm_migration(), "string_len(512)");
821 assert_eq!(FieldType::Image.to_sea_orm_migration(), "string_len(512)");
822 }
823
824 #[test]
827 fn to_shadcn_mappings() {
828 assert_eq!(FieldType::String.to_shadcn(), "Input");
829 assert_eq!(FieldType::Text.to_shadcn(), "Textarea");
830 assert_eq!(FieldType::Bool.to_shadcn(), "Switch");
831 assert_eq!(FieldType::Int32.to_shadcn(), "Input");
832 assert_eq!(FieldType::Uuid.to_shadcn(), "Input");
833 assert_eq!(FieldType::DateTime.to_shadcn(), "Input");
834 assert_eq!(FieldType::Json.to_shadcn(), "Textarea");
835 assert_eq!(FieldType::Enum(vec!["A".into()]).to_shadcn(), "Select");
836 assert_eq!(FieldType::File.to_shadcn(), "FileInput");
837 assert_eq!(FieldType::Image.to_shadcn(), "ImageInput");
838 }
839
840 #[test]
843 fn input_type_mappings() {
844 assert_eq!(FieldType::String.input_type(), "text");
845 assert_eq!(FieldType::Int32.input_type(), "number");
846 assert_eq!(FieldType::Float64.input_type(), "number");
847 assert_eq!(FieldType::Date.input_type(), "date");
848 assert_eq!(FieldType::DateTime.input_type(), "datetime-local");
849 assert_eq!(FieldType::File.input_type(), "file");
850 assert_eq!(FieldType::Image.input_type(), "file");
851 assert_eq!(FieldType::Bool.input_type(), "text");
852 }
853
854 #[test]
857 fn parse_entity_basic_field() {
858 let entity = parse_entity("Post", &["title:string".to_string()]).unwrap();
859 assert_eq!(entity.name, "Post");
860 assert_eq!(entity.fields.len(), 1);
861 assert_eq!(entity.fields[0].name, "title");
862 assert!(matches!(entity.fields[0].field_type, FieldType::String));
863 assert!(!entity.fields[0].optional);
864 assert!(entity.fields[0].relation.is_none());
865 }
866
867 #[test]
870 fn parse_entity_optional_field() {
871 let entity = parse_entity("Post", &["bio:text?".to_string()]).unwrap();
872 assert_eq!(entity.fields.len(), 1);
873 assert!(entity.fields[0].optional);
874 assert!(matches!(entity.fields[0].field_type, FieldType::Text));
875 }
876
877 #[test]
880 fn parse_entity_belongs_to_relation() {
881 let entity = parse_entity("Post", &["author_id:uuid->User".to_string()]).unwrap();
882
883 assert_eq!(entity.fields.len(), 1);
885 assert_eq!(entity.fields[0].name, "author_id");
886 assert!(matches!(entity.fields[0].field_type, FieldType::Uuid));
887 assert_eq!(entity.fields[0].relation.as_deref(), Some("User"));
888
889 assert_eq!(entity.relations.len(), 1);
891 assert_eq!(entity.relations[0].target_entity, "User");
892 assert!(matches!(entity.relations[0].relation_type, RelationType::BelongsTo));
893 assert_eq!(entity.relations[0].fk_column.as_deref(), Some("author_id"));
894 }
895
896 #[test]
899 fn parse_entity_has_many_relation() {
900 let entity = parse_entity("User", &["posts:has_many->Post".to_string()]).unwrap();
901
902 assert_eq!(entity.fields.len(), 0);
904 assert_eq!(entity.relations.len(), 1);
905 assert!(matches!(entity.relations[0].relation_type, RelationType::HasMany));
906 assert_eq!(entity.relations[0].target_entity, "Post");
907 }
908
909 #[test]
912 fn parse_entity_m2m_relation() {
913 let entity = parse_entity("Post", &["tags:m2m->Tag".to_string()]).unwrap();
914
915 assert_eq!(entity.fields.len(), 0);
917 assert_eq!(entity.relations.len(), 1);
918 assert!(matches!(entity.relations[0].relation_type, RelationType::ManyToMany));
919 assert_eq!(entity.relations[0].target_entity, "Tag");
920 }
921
922 #[test]
925 fn parse_entity_validations() {
926 let entity = parse_entity("Post", &["title:string[min=3,max=100]".to_string()]).unwrap();
927 assert_eq!(entity.fields.len(), 1);
928 let validations = &entity.fields[0].validations;
929 assert_eq!(validations.len(), 2);
930 assert!(validations.contains(&ValidationRule::Min(3)));
931 assert!(validations.contains(&ValidationRule::Max(100)));
932 }
933
934 #[test]
937 fn parse_entity_searchable() {
938 let entity = parse_entity("Post", &["title:string[searchable]".to_string()]).unwrap();
939 assert_eq!(entity.fields.len(), 1);
940 assert!(entity.fields[0].searchable);
941 }
942
943 #[test]
946 fn parse_entity_multiple_fields() {
947 let entity = parse_entity(
948 "Product",
949 &[
950 "title:string".to_string(),
951 "price:decimal".to_string(),
952 "description:text?".to_string(),
953 "category_id:uuid->Category".to_string(),
954 "tags:m2m->Tag".to_string(),
955 ],
956 )
957 .unwrap();
958
959 assert_eq!(entity.name, "Product");
960 assert_eq!(entity.fields.len(), 4);
962 assert_eq!(entity.relations.len(), 2);
964 }
965
966 #[test]
969 fn parse_entity_enum_field() {
970 let entity = parse_entity(
971 "Post",
972 &["status:enum(draft,published,archived)".to_string()],
973 )
974 .unwrap();
975 assert_eq!(entity.fields.len(), 1);
976 assert_eq!(entity.fields[0].name, "status");
977 assert!(matches!(
978 entity.fields[0].field_type,
979 FieldType::Enum(ref v) if v == &["draft", "published", "archived"]
980 ));
981 }
982
983 #[test]
986 fn parse_entity_invalid_format() {
987 assert!(parse_entity("Post", &["invalid_no_colon".to_string()]).is_err());
988 }
989
990 #[test]
993 fn parse_entity_unknown_field_type() {
994 assert!(parse_entity("Post", &["name:foobar".to_string()]).is_err());
995 }
996
997 #[test]
1000 fn parse_validations_mixed() {
1001 let rules = parse_validations("min=5,max=200,email,unique").unwrap();
1002 assert_eq!(rules.len(), 4);
1003 assert!(rules.contains(&ValidationRule::Min(5)));
1004 assert!(rules.contains(&ValidationRule::Max(200)));
1005 assert!(rules.contains(&ValidationRule::Email));
1006 assert!(rules.contains(&ValidationRule::Unique));
1007 }
1008
1009 #[test]
1010 fn parse_validations_regex() {
1011 let rules = parse_validations("regex=^[a-z]+$").unwrap();
1012 assert_eq!(rules.len(), 1);
1013 assert!(matches!(&rules[0], ValidationRule::Regex(r) if r == "^[a-z]+$"));
1014 }
1015
1016 #[test]
1017 fn parse_validations_empty() {
1018 let rules = parse_validations("").unwrap();
1019 assert!(rules.is_empty());
1020 }
1021
1022 #[test]
1023 fn parse_validations_invalid_regex_errors() {
1024 let result = parse_validations("regex=[invalid");
1025 assert!(result.is_err());
1026 assert!(result.unwrap_err().to_string().contains("Invalid regex pattern"));
1027 }
1028
1029 #[test]
1030 fn parse_validations_valid_regex_succeeds() {
1031 let rules = parse_validations("regex=^\\d{3}-\\d{4}$").unwrap();
1032 assert_eq!(rules.len(), 1);
1033 assert!(matches!(&rules[0], ValidationRule::Regex(r) if r == "^\\d{3}-\\d{4}$"));
1034 }
1035
1036 #[test]
1037 fn parse_validations_invalid_min_errors() {
1038 let result = parse_validations("min=abc");
1039 assert!(result.is_err());
1040 assert!(result.unwrap_err().to_string().contains("Invalid min value"));
1041 }
1042
1043 #[test]
1044 fn parse_validations_negative_max_errors() {
1045 let result = parse_validations("max=-5");
1046 assert!(result.is_err());
1047 assert!(result.unwrap_err().to_string().contains("Invalid max value"));
1048 }
1049
1050 #[test]
1051 fn parse_validations_invalid_max_errors() {
1052 let result = parse_validations("max=not_a_number");
1053 assert!(result.is_err());
1054 assert!(result.unwrap_err().to_string().contains("Invalid max value"));
1055 }
1056
1057 #[test]
1060 fn parse_entity_optional_belongs_to() {
1061 let entity = parse_entity("Post", &["category_id:uuid->Category?".to_string()]).unwrap();
1062 assert_eq!(entity.fields.len(), 1);
1063 assert!(entity.fields[0].optional);
1064 assert_eq!(entity.relations.len(), 1);
1065 assert!(entity.relations[0].optional);
1066 }
1067
1068 #[test]
1071 fn parse_entity_optional_fk_question_before_arrow() {
1072 let entity = parse_entity("Post", &["category_id:uuid?->Category".to_string()]).unwrap();
1074 assert_eq!(entity.fields.len(), 1);
1075 assert!(entity.fields[0].optional);
1076 assert_eq!(entity.fields[0].relation.as_deref(), Some("Category"));
1077 assert!(matches!(entity.fields[0].field_type, FieldType::Uuid));
1078 assert_eq!(entity.relations.len(), 1);
1079 assert!(entity.relations[0].optional);
1080 }
1081
1082 #[test]
1083 fn parse_entity_optional_fk_question_at_end() {
1084 let entity = parse_entity("Post", &["category_id:uuid->Category?".to_string()]).unwrap();
1086 assert_eq!(entity.fields.len(), 1);
1087 assert!(entity.fields[0].optional);
1088 }
1089
1090 #[test]
1093 fn parse_entity_validation_with_relation() {
1094 let entity = parse_entity("Post", &["author_id:uuid[required]->User".to_string()]).unwrap();
1095 assert_eq!(entity.fields.len(), 1);
1096 assert_eq!(entity.fields[0].relation.as_deref(), Some("User"));
1097 assert!(entity.fields[0].validations.contains(&ValidationRule::Required));
1098 }
1099}