use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct TypeAttribute {
pub description: Option<String>,
pub deprecated: bool,
pub rename: Option<String>,
pub rename_all: Option<RenameRule>,
pub examples: Vec<serde_json::Value>,
pub format_metadata: IndexMap<String, IndexMap<String, serde_json::Value>>,
}
impl TypeAttribute {
pub fn new() -> Self {
Self::default()
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn deprecated(mut self) -> Self {
self.deprecated = true;
self
}
pub fn with_rename(mut self, name: impl Into<String>) -> Self {
self.rename = Some(name.into());
self
}
pub fn with_rename_all(mut self, rule: RenameRule) -> Self {
self.rename_all = Some(rule);
self
}
pub fn with_example(mut self, example: serde_json::Value) -> Self {
self.examples.push(example);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct FieldAttribute {
pub description: Option<String>,
pub deprecated: bool,
pub rename: Option<String>,
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub minimum: Option<f64>,
pub maximum: Option<f64>,
pub exclusive_minimum: Option<f64>,
pub exclusive_maximum: Option<f64>,
pub multiple_of: Option<f64>,
pub pattern: Option<String>,
pub format: Option<String>,
pub default: Option<serde_json::Value>,
pub skip: bool,
pub skip_formats: Vec<String>,
pub flatten: bool,
pub nullable: Option<bool>,
pub format_renames: IndexMap<String, String>,
pub examples: Vec<serde_json::Value>,
pub read_only: bool,
pub write_only: bool,
pub format_metadata: IndexMap<String, IndexMap<String, serde_json::Value>>,
}
impl FieldAttribute {
pub fn new() -> Self {
Self::default()
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn deprecated(mut self) -> Self {
self.deprecated = true;
self
}
pub fn with_rename(mut self, name: impl Into<String>) -> Self {
self.rename = Some(name.into());
self
}
pub fn with_min_length(mut self, len: usize) -> Self {
self.min_length = Some(len);
self
}
pub fn with_max_length(mut self, len: usize) -> Self {
self.max_length = Some(len);
self
}
pub fn with_length(mut self, min: Option<usize>, max: Option<usize>) -> Self {
self.min_length = min;
self.max_length = max;
self
}
pub fn with_minimum(mut self, min: f64) -> Self {
self.minimum = Some(min);
self
}
pub fn with_maximum(mut self, max: f64) -> Self {
self.maximum = Some(max);
self
}
pub fn with_range(mut self, min: Option<f64>, max: Option<f64>) -> Self {
self.minimum = min;
self.maximum = max;
self
}
pub fn with_pattern(mut self, pattern: impl Into<String>) -> Self {
self.pattern = Some(pattern.into());
self
}
pub fn with_format(mut self, format: impl Into<String>) -> Self {
self.format = Some(format.into());
self
}
pub fn with_default(mut self, default: serde_json::Value) -> Self {
self.default = Some(default);
self
}
pub fn skip(mut self) -> Self {
self.skip = true;
self
}
pub fn skip_for(mut self, formats: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.skip_formats = formats.into_iter().map(|f| f.into()).collect();
self
}
pub fn flatten(mut self) -> Self {
self.flatten = true;
self
}
pub fn nullable(mut self, nullable: bool) -> Self {
self.nullable = Some(nullable);
self
}
pub fn rename_for(mut self, format: impl Into<String>, name: impl Into<String>) -> Self {
self.format_renames.insert(format.into(), name.into());
self
}
pub fn with_example(mut self, example: serde_json::Value) -> Self {
self.examples.push(example);
self
}
pub fn read_only(mut self) -> Self {
self.read_only = true;
self
}
pub fn write_only(mut self) -> Self {
self.write_only = true;
self
}
pub fn effective_name<'a>(&'a self, format: &str, original: &'a str) -> &'a str {
self.format_renames
.get(format)
.map(|s| s.as_str())
.or(self.rename.as_deref())
.unwrap_or(original)
}
pub fn should_skip_for(&self, format: &str) -> bool {
self.skip || self.skip_formats.iter().any(|f| f == format)
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct EnumAttribute {
#[serde(flatten)]
pub type_attr: TypeAttribute,
pub representation: Option<EnumRepresentationAttr>,
}
impl EnumAttribute {
pub fn new() -> Self {
Self::default()
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.type_attr.description = Some(description.into());
self
}
pub fn with_representation(mut self, repr: EnumRepresentationAttr) -> Self {
self.representation = Some(repr);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EnumRepresentationAttr {
External,
Internal { tag: String },
Adjacent { tag: String, content: String },
Untagged,
}
impl Default for EnumRepresentationAttr {
fn default() -> Self {
EnumRepresentationAttr::External
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct VariantAttribute {
pub description: Option<String>,
pub deprecated: bool,
pub rename: Option<String>,
pub skip: bool,
pub format_renames: IndexMap<String, String>,
pub aliases: Vec<String>,
pub format_metadata: IndexMap<String, IndexMap<String, serde_json::Value>>,
}
impl VariantAttribute {
pub fn new() -> Self {
Self::default()
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn deprecated(mut self) -> Self {
self.deprecated = true;
self
}
pub fn with_rename(mut self, name: impl Into<String>) -> Self {
self.rename = Some(name.into());
self
}
pub fn skip(mut self) -> Self {
self.skip = true;
self
}
pub fn rename_for(mut self, format: impl Into<String>, name: impl Into<String>) -> Self {
self.format_renames.insert(format.into(), name.into());
self
}
pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
self.aliases.push(alias.into());
self
}
pub fn effective_name<'a>(&'a self, format: &str, original: &'a str) -> &'a str {
self.format_renames
.get(format)
.map(|s| s.as_str())
.or(self.rename.as_deref())
.unwrap_or(original)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RenameRule {
None,
Lowercase,
Uppercase,
PascalCase,
CamelCase,
SnakeCase,
ScreamingSnakeCase,
KebabCase,
ScreamingKebabCase,
}
impl RenameRule {
pub fn apply(&self, s: &str) -> String {
match self {
RenameRule::None => s.to_string(),
RenameRule::Lowercase => s.to_lowercase(),
RenameRule::Uppercase => s.to_uppercase(),
RenameRule::PascalCase => to_pascal_case(s),
RenameRule::CamelCase => to_camel_case(s),
RenameRule::SnakeCase => to_snake_case(s),
RenameRule::ScreamingSnakeCase => to_snake_case(s).to_uppercase(),
RenameRule::KebabCase => to_snake_case(s).replace('_', "-"),
RenameRule::ScreamingKebabCase => to_snake_case(s).replace('_', "-").to_uppercase(),
}
}
}
impl Default for RenameRule {
fn default() -> Self {
RenameRule::None
}
}
fn to_pascal_case(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut capitalize_next = true;
for c in s.chars() {
if c == '_' || c == '-' || c == ' ' {
capitalize_next = true;
} else if capitalize_next {
result.extend(c.to_uppercase());
capitalize_next = false;
} else {
result.push(c);
}
}
result
}
fn to_camel_case(s: &str) -> String {
let pascal = to_pascal_case(s);
let mut chars = pascal.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_lowercase().chain(chars).collect(),
}
}
fn to_snake_case(s: &str) -> String {
let mut result = String::with_capacity(s.len() + 4);
let mut prev_was_uppercase = false;
let mut prev_was_separator = true;
for c in s.chars() {
if c == '-' || c == ' ' {
result.push('_');
prev_was_separator = true;
prev_was_uppercase = false;
} else if c.is_uppercase() {
if !prev_was_separator && !prev_was_uppercase {
result.push('_');
}
result.extend(c.to_lowercase());
prev_was_uppercase = true;
prev_was_separator = false;
} else {
result.push(c);
prev_was_uppercase = false;
prev_was_separator = c == '_';
}
}
result
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct SchemaAttributes {
pub type_attr: TypeAttribute,
pub field_attrs: IndexMap<String, FieldAttribute>,
pub variant_attrs: IndexMap<String, VariantAttribute>,
pub enum_attr: Option<EnumAttribute>,
}
impl SchemaAttributes {
pub fn new() -> Self {
Self::default()
}
pub fn with_type_attr(mut self, attr: TypeAttribute) -> Self {
self.type_attr = attr;
self
}
pub fn with_field_attr(mut self, field: impl Into<String>, attr: FieldAttribute) -> Self {
self.field_attrs.insert(field.into(), attr);
self
}
pub fn with_variant_attr(mut self, variant: impl Into<String>, attr: VariantAttribute) -> Self {
self.variant_attrs.insert(variant.into(), attr);
self
}
pub fn with_enum_attr(mut self, attr: EnumAttribute) -> Self {
self.enum_attr = Some(attr);
self
}
pub fn get_field_attr(&self, field: &str) -> &FieldAttribute {
static DEFAULT: once_cell::sync::Lazy<FieldAttribute> =
once_cell::sync::Lazy::new(FieldAttribute::default);
self.field_attrs.get(field).unwrap_or(&DEFAULT)
}
pub fn get_variant_attr(&self, variant: &str) -> &VariantAttribute {
static DEFAULT: once_cell::sync::Lazy<VariantAttribute> =
once_cell::sync::Lazy::new(VariantAttribute::default);
self.variant_attrs.get(variant).unwrap_or(&DEFAULT)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rename_rules() {
assert_eq!(RenameRule::CamelCase.apply("user_name"), "userName");
assert_eq!(RenameRule::PascalCase.apply("user_name"), "UserName");
assert_eq!(RenameRule::SnakeCase.apply("userName"), "user_name");
assert_eq!(
RenameRule::ScreamingSnakeCase.apply("userName"),
"USER_NAME"
);
assert_eq!(RenameRule::KebabCase.apply("user_name"), "user-name");
assert_eq!(RenameRule::Lowercase.apply("UserName"), "username");
assert_eq!(RenameRule::Uppercase.apply("userName"), "USERNAME");
}
#[test]
fn test_field_attribute_effective_name() {
let attr = FieldAttribute::new()
.with_rename("defaultName")
.rename_for("graphql", "graphqlName")
.rename_for("typescript", "tsName");
assert_eq!(attr.effective_name("graphql", "original"), "graphqlName");
assert_eq!(attr.effective_name("typescript", "original"), "tsName");
assert_eq!(attr.effective_name("json-schema", "original"), "defaultName");
}
#[test]
fn test_field_attribute_skip() {
let attr = FieldAttribute::new().skip_for(["graphql", "protobuf"]);
assert!(attr.should_skip_for("graphql"));
assert!(attr.should_skip_for("protobuf"));
assert!(!attr.should_skip_for("json-schema"));
}
#[test]
fn test_type_attribute_builder() {
let attr = TypeAttribute::new()
.with_description("A user type")
.with_rename("UserDTO")
.with_rename_all(RenameRule::CamelCase)
.deprecated();
assert_eq!(attr.description, Some("A user type".to_string()));
assert_eq!(attr.rename, Some("UserDTO".to_string()));
assert_eq!(attr.rename_all, Some(RenameRule::CamelCase));
assert!(attr.deprecated);
}
#[test]
fn test_schema_attributes() {
let attrs = SchemaAttributes::new()
.with_type_attr(TypeAttribute::new().with_description("Test type"))
.with_field_attr(
"name",
FieldAttribute::new()
.with_min_length(1)
.with_max_length(100),
)
.with_field_attr("email", FieldAttribute::new().with_format("email"));
assert!(attrs.type_attr.description.is_some());
assert_eq!(attrs.get_field_attr("name").min_length, Some(1));
assert_eq!(
attrs.get_field_attr("email").format,
Some("email".to_string())
);
assert!(attrs.get_field_attr("unknown").min_length.is_none());
}
}