Skip to main content

alembic_engine/
mapping.rs

1//! Shared mapping helpers used by multiple adapters.
2
3use alembic_core::{FieldSchema, FieldType};
4use anyhow::{anyhow, Result};
5use serde_json::Value;
6
7/// Convert a human-readable label into a URL-safe slug.
8///
9/// Lowercases all characters, replaces runs of non-alphanumeric characters
10/// with a single dash, and strips leading/trailing dashes.
11pub 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
33/// Map an Alembic field type to the backend custom-field type string.
34pub 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
46/// Extract tag names from a JSON value returned by a backend.
47///
48/// Accepts arrays of strings or objects with `"name"` / `"slug"` fields,
49/// and returns the collected tag names.
50pub 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                }
66            }
67            _ => {}
68        }
69    }
70    Ok(tags)
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use serde_json::json;
77
78    #[test]
79    fn test_slugify() {
80        assert_eq!(slugify("Hello World"), "hello-world");
81        assert_eq!(slugify("EVPN Fabric!"), "evpn-fabric");
82        assert_eq!(slugify("---test---"), "test");
83    }
84
85    #[test]
86    fn test_custom_field_type_for_schema() {
87        let schema = |r#type| FieldSchema {
88            r#type,
89            required: false,
90            nullable: true,
91            description: None,
92            format: None,
93            pattern: None,
94        };
95        assert_eq!(
96            custom_field_type_for_schema(&schema(FieldType::String)),
97            "text"
98        );
99        assert_eq!(
100            custom_field_type_for_schema(&schema(FieldType::Int)),
101            "integer"
102        );
103        assert_eq!(
104            custom_field_type_for_schema(&schema(FieldType::Float)),
105            "decimal"
106        );
107        assert_eq!(
108            custom_field_type_for_schema(&schema(FieldType::Bool)),
109            "boolean"
110        );
111        assert_eq!(
112            custom_field_type_for_schema(&schema(FieldType::Json)),
113            "json"
114        );
115    }
116
117    #[test]
118    fn test_tags_from_value_array_of_strings() {
119        let val = json!(["tag1", "tag2"]);
120        assert_eq!(tags_from_value(&val).unwrap(), vec!["tag1", "tag2"]);
121    }
122
123    #[test]
124    fn test_tags_from_value_array_of_objects() {
125        let val = json!([{"name": "tag1"}, {"slug": "tag-2"}]);
126        assert_eq!(tags_from_value(&val).unwrap(), vec!["tag1", "tag-2"]);
127    }
128
129    #[test]
130    fn test_tags_from_value_null() {
131        let val = json!(null);
132        assert_eq!(tags_from_value(&val).unwrap(), Vec::<String>::new());
133    }
134
135    #[test]
136    fn test_tags_from_value_invalid() {
137        let val = json!("not an array");
138        assert!(tags_from_value(&val).is_err());
139    }
140}