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}