alembic_engine/
mapping.rs1use alembic_core::{FieldSchema, FieldType};
4use anyhow::{anyhow, Result};
5use serde_json::Value;
6
7pub fn slugify(input: &str) -> String {
12 let mut out = String::new();
13 let mut last_dash = false;
14 for ch in input.chars() {
15 let lower = ch.to_ascii_lowercase();
16 if lower.is_ascii_alphanumeric() {
17 out.push(lower);
18 last_dash = false;
19 } else if !last_dash {
20 out.push('-');
21 last_dash = true;
22 }
23 }
24 while out.ends_with('-') {
25 out.pop();
26 }
27 while out.starts_with('-') {
28 out.remove(0);
29 }
30 out
31}
32
33pub fn custom_field_type_for_schema(field: &FieldSchema) -> String {
35 match field.r#type {
36 FieldType::Int => "integer".to_string(),
37 FieldType::Float => "decimal".to_string(),
38 FieldType::Bool => "boolean".to_string(),
39 FieldType::Date => "date".to_string(),
40 FieldType::Datetime => "datetime".to_string(),
41 FieldType::Json | FieldType::List { .. } | FieldType::Map { .. } => "json".to_string(),
42 _ => "text".to_string(),
43 }
44}
45
46pub fn tags_from_value(value: &Value) -> Result<Vec<String>> {
51 let items = match value {
52 Value::Array(items) => items,
53 Value::Null => return Ok(Vec::new()),
54 _ => return Err(anyhow!("tags must be an array")),
55 };
56 let mut tags = Vec::new();
57 for item in items {
58 match item {
59 Value::String(name) => tags.push(name.clone()),
60 Value::Object(map) => {
61 if let Some(Value::String(name)) = map.get("name") {
62 tags.push(name.clone());
63 } else if let Some(Value::String(slug)) = map.get("slug") {
64 tags.push(slug.clone());
65 } else {
66 return Err(anyhow!(
67 "tag object must have a string name or slug: {item}"
68 ));
69 }
70 }
71 _ => return Err(anyhow!("tag item must be a string or object: {item}")),
72 }
73 }
74 Ok(tags)
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use serde_json::json;
81
82 #[test]
83 fn test_slugify() {
84 assert_eq!(slugify("Hello World"), "hello-world");
85 assert_eq!(slugify("EVPN Fabric!"), "evpn-fabric");
86 assert_eq!(slugify("---test---"), "test");
87 }
88
89 #[test]
90 fn test_custom_field_type_for_schema() {
91 let schema = |r#type| FieldSchema {
92 r#type,
93 required: false,
94 nullable: true,
95 description: None,
96 format: None,
97 pattern: None,
98 };
99 assert_eq!(
100 custom_field_type_for_schema(&schema(FieldType::String)),
101 "text"
102 );
103 assert_eq!(
104 custom_field_type_for_schema(&schema(FieldType::Int)),
105 "integer"
106 );
107 assert_eq!(
108 custom_field_type_for_schema(&schema(FieldType::Float)),
109 "decimal"
110 );
111 assert_eq!(
112 custom_field_type_for_schema(&schema(FieldType::Bool)),
113 "boolean"
114 );
115 assert_eq!(
116 custom_field_type_for_schema(&schema(FieldType::Json)),
117 "json"
118 );
119 assert_eq!(
120 custom_field_type_for_schema(&schema(FieldType::Date)),
121 "date"
122 );
123 assert_eq!(
124 custom_field_type_for_schema(&schema(FieldType::Datetime)),
125 "datetime"
126 );
127 }
128
129 #[test]
130 fn test_tags_from_value_array_of_strings() {
131 let val = json!(["tag1", "tag2"]);
132 assert_eq!(tags_from_value(&val).unwrap(), vec!["tag1", "tag2"]);
133 }
134
135 #[test]
136 fn test_tags_from_value_array_of_objects() {
137 let val = json!([{"name": "tag1"}, {"slug": "tag-2"}]);
138 assert_eq!(tags_from_value(&val).unwrap(), vec!["tag1", "tag-2"]);
139 }
140
141 #[test]
142 fn test_tags_from_value_null() {
143 let val = json!(null);
144 assert_eq!(tags_from_value(&val).unwrap(), Vec::<String>::new());
145 }
146
147 #[test]
148 fn test_tags_from_value_invalid() {
149 let val = json!("not an array");
150 assert!(tags_from_value(&val).is_err());
151 }
152
153 #[test]
154 fn test_tags_from_value_non_string_non_object_item() {
155 assert!(tags_from_value(&json!([1, 2])).is_err());
156 assert!(tags_from_value(&json!(["ok", 5])).is_err());
157 }
158
159 #[test]
160 fn test_tags_from_value_object_without_name_or_slug() {
161 assert!(tags_from_value(&json!([{"id": 5}])).is_err());
162 }
163}