omni_schema_core/
attributes.rs

1
2use indexmap::IndexMap;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
6pub struct TypeAttribute {
7    pub description: Option<String>,
8    pub deprecated: bool,
9    pub rename: Option<String>,
10    pub rename_all: Option<RenameRule>,
11    pub examples: Vec<serde_json::Value>,
12    pub format_metadata: IndexMap<String, IndexMap<String, serde_json::Value>>,
13}
14
15impl TypeAttribute {
16    pub fn new() -> Self {
17        Self::default()
18    }
19
20    pub fn with_description(mut self, description: impl Into<String>) -> Self {
21        self.description = Some(description.into());
22        self
23    }
24
25    pub fn deprecated(mut self) -> Self {
26        self.deprecated = true;
27        self
28    }
29
30    pub fn with_rename(mut self, name: impl Into<String>) -> Self {
31        self.rename = Some(name.into());
32        self
33    }
34
35    pub fn with_rename_all(mut self, rule: RenameRule) -> Self {
36        self.rename_all = Some(rule);
37        self
38    }
39
40    pub fn with_example(mut self, example: serde_json::Value) -> Self {
41        self.examples.push(example);
42        self
43    }
44}
45
46#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
47pub struct FieldAttribute {
48    pub description: Option<String>,
49    pub deprecated: bool,
50    pub rename: Option<String>,
51    pub min_length: Option<usize>,
52    pub max_length: Option<usize>,
53    pub minimum: Option<f64>,
54    pub maximum: Option<f64>,
55    pub exclusive_minimum: Option<f64>,
56    pub exclusive_maximum: Option<f64>,
57    pub multiple_of: Option<f64>,
58    pub pattern: Option<String>,
59    pub format: Option<String>,
60    pub default: Option<serde_json::Value>,
61    pub skip: bool,
62    pub skip_formats: Vec<String>,
63    pub flatten: bool,
64    pub nullable: Option<bool>,
65    pub format_renames: IndexMap<String, String>,
66    pub examples: Vec<serde_json::Value>,
67    pub read_only: bool,
68    pub write_only: bool,
69    pub format_metadata: IndexMap<String, IndexMap<String, serde_json::Value>>,
70}
71
72impl FieldAttribute {
73    pub fn new() -> Self {
74        Self::default()
75    }
76
77    pub fn with_description(mut self, description: impl Into<String>) -> Self {
78        self.description = Some(description.into());
79        self
80    }
81
82    pub fn deprecated(mut self) -> Self {
83        self.deprecated = true;
84        self
85    }
86
87    pub fn with_rename(mut self, name: impl Into<String>) -> Self {
88        self.rename = Some(name.into());
89        self
90    }
91
92    pub fn with_min_length(mut self, len: usize) -> Self {
93        self.min_length = Some(len);
94        self
95    }
96
97    pub fn with_max_length(mut self, len: usize) -> Self {
98        self.max_length = Some(len);
99        self
100    }
101
102    pub fn with_length(mut self, min: Option<usize>, max: Option<usize>) -> Self {
103        self.min_length = min;
104        self.max_length = max;
105        self
106    }
107
108    pub fn with_minimum(mut self, min: f64) -> Self {
109        self.minimum = Some(min);
110        self
111    }
112
113    pub fn with_maximum(mut self, max: f64) -> Self {
114        self.maximum = Some(max);
115        self
116    }
117
118    pub fn with_range(mut self, min: Option<f64>, max: Option<f64>) -> Self {
119        self.minimum = min;
120        self.maximum = max;
121        self
122    }
123
124    pub fn with_pattern(mut self, pattern: impl Into<String>) -> Self {
125        self.pattern = Some(pattern.into());
126        self
127    }
128
129    pub fn with_format(mut self, format: impl Into<String>) -> Self {
130        self.format = Some(format.into());
131        self
132    }
133
134    pub fn with_default(mut self, default: serde_json::Value) -> Self {
135        self.default = Some(default);
136        self
137    }
138
139    pub fn skip(mut self) -> Self {
140        self.skip = true;
141        self
142    }
143
144    pub fn skip_for(mut self, formats: impl IntoIterator<Item = impl Into<String>>) -> Self {
145        self.skip_formats = formats.into_iter().map(|f| f.into()).collect();
146        self
147    }
148
149    pub fn flatten(mut self) -> Self {
150        self.flatten = true;
151        self
152    }
153
154    pub fn nullable(mut self, nullable: bool) -> Self {
155        self.nullable = Some(nullable);
156        self
157    }
158
159    pub fn rename_for(mut self, format: impl Into<String>, name: impl Into<String>) -> Self {
160        self.format_renames.insert(format.into(), name.into());
161        self
162    }
163
164    pub fn with_example(mut self, example: serde_json::Value) -> Self {
165        self.examples.push(example);
166        self
167    }
168
169    pub fn read_only(mut self) -> Self {
170        self.read_only = true;
171        self
172    }
173
174    pub fn write_only(mut self) -> Self {
175        self.write_only = true;
176        self
177    }
178
179    pub fn effective_name<'a>(&'a self, format: &str, original: &'a str) -> &'a str {
180        self.format_renames
181            .get(format)
182            .map(|s| s.as_str())
183            .or(self.rename.as_deref())
184            .unwrap_or(original)
185    }
186
187    pub fn should_skip_for(&self, format: &str) -> bool {
188        self.skip || self.skip_formats.iter().any(|f| f == format)
189    }
190}
191
192#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
193pub struct EnumAttribute {
194    #[serde(flatten)]
195    pub type_attr: TypeAttribute,
196    pub representation: Option<EnumRepresentationAttr>,
197}
198
199impl EnumAttribute {
200    pub fn new() -> Self {
201        Self::default()
202    }
203
204    pub fn with_description(mut self, description: impl Into<String>) -> Self {
205        self.type_attr.description = Some(description.into());
206        self
207    }
208
209    pub fn with_representation(mut self, repr: EnumRepresentationAttr) -> Self {
210        self.representation = Some(repr);
211        self
212    }
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216pub enum EnumRepresentationAttr {
217    External,
218    Internal { tag: String },
219    Adjacent { tag: String, content: String },
220    Untagged,
221}
222
223impl Default for EnumRepresentationAttr {
224    fn default() -> Self {
225        EnumRepresentationAttr::External
226    }
227}
228
229#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
230pub struct VariantAttribute {
231    pub description: Option<String>,
232    pub deprecated: bool,
233    pub rename: Option<String>,
234    pub skip: bool,
235    pub format_renames: IndexMap<String, String>,
236    pub aliases: Vec<String>,
237    pub format_metadata: IndexMap<String, IndexMap<String, serde_json::Value>>,
238}
239
240impl VariantAttribute {
241    pub fn new() -> Self {
242        Self::default()
243    }
244
245    pub fn with_description(mut self, description: impl Into<String>) -> Self {
246        self.description = Some(description.into());
247        self
248    }
249
250    pub fn deprecated(mut self) -> Self {
251        self.deprecated = true;
252        self
253    }
254
255    pub fn with_rename(mut self, name: impl Into<String>) -> Self {
256        self.rename = Some(name.into());
257        self
258    }
259
260    pub fn skip(mut self) -> Self {
261        self.skip = true;
262        self
263    }
264
265    pub fn rename_for(mut self, format: impl Into<String>, name: impl Into<String>) -> Self {
266        self.format_renames.insert(format.into(), name.into());
267        self
268    }
269
270    pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
271        self.aliases.push(alias.into());
272        self
273    }
274
275    pub fn effective_name<'a>(&'a self, format: &str, original: &'a str) -> &'a str {
276        self.format_renames
277            .get(format)
278            .map(|s| s.as_str())
279            .or(self.rename.as_deref())
280            .unwrap_or(original)
281    }
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
285#[serde(rename_all = "snake_case")]
286pub enum RenameRule {
287    None,
288    Lowercase,
289    Uppercase,
290    PascalCase,
291    CamelCase,
292    SnakeCase,
293    ScreamingSnakeCase,
294    KebabCase,
295    ScreamingKebabCase,
296}
297
298impl RenameRule {
299    pub fn apply(&self, s: &str) -> String {
300        match self {
301            RenameRule::None => s.to_string(),
302            RenameRule::Lowercase => s.to_lowercase(),
303            RenameRule::Uppercase => s.to_uppercase(),
304            RenameRule::PascalCase => to_pascal_case(s),
305            RenameRule::CamelCase => to_camel_case(s),
306            RenameRule::SnakeCase => to_snake_case(s),
307            RenameRule::ScreamingSnakeCase => to_snake_case(s).to_uppercase(),
308            RenameRule::KebabCase => to_snake_case(s).replace('_', "-"),
309            RenameRule::ScreamingKebabCase => to_snake_case(s).replace('_', "-").to_uppercase(),
310        }
311    }
312}
313
314impl Default for RenameRule {
315    fn default() -> Self {
316        RenameRule::None
317    }
318}
319
320fn to_pascal_case(s: &str) -> String {
321    let mut result = String::with_capacity(s.len());
322    let mut capitalize_next = true;
323
324    for c in s.chars() {
325        if c == '_' || c == '-' || c == ' ' {
326            capitalize_next = true;
327        } else if capitalize_next {
328            result.extend(c.to_uppercase());
329            capitalize_next = false;
330        } else {
331            result.push(c);
332        }
333    }
334
335    result
336}
337
338fn to_camel_case(s: &str) -> String {
339    let pascal = to_pascal_case(s);
340    let mut chars = pascal.chars();
341    match chars.next() {
342        None => String::new(),
343        Some(first) => first.to_lowercase().chain(chars).collect(),
344    }
345}
346
347fn to_snake_case(s: &str) -> String {
348    let mut result = String::with_capacity(s.len() + 4);
349    let mut prev_was_uppercase = false;
350    let mut prev_was_separator = true;
351
352    for c in s.chars() {
353        if c == '-' || c == ' ' {
354            result.push('_');
355            prev_was_separator = true;
356            prev_was_uppercase = false;
357        } else if c.is_uppercase() {
358            if !prev_was_separator && !prev_was_uppercase {
359                result.push('_');
360            }
361            result.extend(c.to_lowercase());
362            prev_was_uppercase = true;
363            prev_was_separator = false;
364        } else {
365            result.push(c);
366            prev_was_uppercase = false;
367            prev_was_separator = c == '_';
368        }
369    }
370
371    result
372}
373
374#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
375pub struct SchemaAttributes {
376    pub type_attr: TypeAttribute,
377    pub field_attrs: IndexMap<String, FieldAttribute>,
378    pub variant_attrs: IndexMap<String, VariantAttribute>,
379    pub enum_attr: Option<EnumAttribute>,
380}
381
382impl SchemaAttributes {
383    pub fn new() -> Self {
384        Self::default()
385    }
386
387    pub fn with_type_attr(mut self, attr: TypeAttribute) -> Self {
388        self.type_attr = attr;
389        self
390    }
391
392    pub fn with_field_attr(mut self, field: impl Into<String>, attr: FieldAttribute) -> Self {
393        self.field_attrs.insert(field.into(), attr);
394        self
395    }
396
397    pub fn with_variant_attr(mut self, variant: impl Into<String>, attr: VariantAttribute) -> Self {
398        self.variant_attrs.insert(variant.into(), attr);
399        self
400    }
401
402    pub fn with_enum_attr(mut self, attr: EnumAttribute) -> Self {
403        self.enum_attr = Some(attr);
404        self
405    }
406
407    pub fn get_field_attr(&self, field: &str) -> &FieldAttribute {
408        static DEFAULT: once_cell::sync::Lazy<FieldAttribute> =
409            once_cell::sync::Lazy::new(FieldAttribute::default);
410        self.field_attrs.get(field).unwrap_or(&DEFAULT)
411    }
412
413    pub fn get_variant_attr(&self, variant: &str) -> &VariantAttribute {
414        static DEFAULT: once_cell::sync::Lazy<VariantAttribute> =
415            once_cell::sync::Lazy::new(VariantAttribute::default);
416        self.variant_attrs.get(variant).unwrap_or(&DEFAULT)
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    #[test]
425    fn test_rename_rules() {
426        assert_eq!(RenameRule::CamelCase.apply("user_name"), "userName");
427        assert_eq!(RenameRule::PascalCase.apply("user_name"), "UserName");
428        assert_eq!(RenameRule::SnakeCase.apply("userName"), "user_name");
429        assert_eq!(
430            RenameRule::ScreamingSnakeCase.apply("userName"),
431            "USER_NAME"
432        );
433        assert_eq!(RenameRule::KebabCase.apply("user_name"), "user-name");
434        assert_eq!(RenameRule::Lowercase.apply("UserName"), "username");
435        assert_eq!(RenameRule::Uppercase.apply("userName"), "USERNAME");
436    }
437
438    #[test]
439    fn test_field_attribute_effective_name() {
440        let attr = FieldAttribute::new()
441            .with_rename("defaultName")
442            .rename_for("graphql", "graphqlName")
443            .rename_for("typescript", "tsName");
444
445        assert_eq!(attr.effective_name("graphql", "original"), "graphqlName");
446        assert_eq!(attr.effective_name("typescript", "original"), "tsName");
447        assert_eq!(attr.effective_name("json-schema", "original"), "defaultName");
448    }
449
450    #[test]
451    fn test_field_attribute_skip() {
452        let attr = FieldAttribute::new().skip_for(["graphql", "protobuf"]);
453
454        assert!(attr.should_skip_for("graphql"));
455        assert!(attr.should_skip_for("protobuf"));
456        assert!(!attr.should_skip_for("json-schema"));
457    }
458
459    #[test]
460    fn test_type_attribute_builder() {
461        let attr = TypeAttribute::new()
462            .with_description("A user type")
463            .with_rename("UserDTO")
464            .with_rename_all(RenameRule::CamelCase)
465            .deprecated();
466
467        assert_eq!(attr.description, Some("A user type".to_string()));
468        assert_eq!(attr.rename, Some("UserDTO".to_string()));
469        assert_eq!(attr.rename_all, Some(RenameRule::CamelCase));
470        assert!(attr.deprecated);
471    }
472
473    #[test]
474    fn test_schema_attributes() {
475        let attrs = SchemaAttributes::new()
476            .with_type_attr(TypeAttribute::new().with_description("Test type"))
477            .with_field_attr(
478                "name",
479                FieldAttribute::new()
480                    .with_min_length(1)
481                    .with_max_length(100),
482            )
483            .with_field_attr("email", FieldAttribute::new().with_format("email"));
484
485        assert!(attrs.type_attr.description.is_some());
486        assert_eq!(attrs.get_field_attr("name").min_length, Some(1));
487        assert_eq!(
488            attrs.get_field_attr("email").format,
489            Some("email".to_string())
490        );
491        assert!(attrs.get_field_attr("unknown").min_length.is_none());
492    }
493}