feat(framework): Added smithy, smithy-core and smithy-generator crates (#9249)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Debarshi Gupta
2025-09-26 12:55:39 +05:30
committed by GitHub
parent 8e629abc92
commit 0baae338d3
10 changed files with 2093 additions and 0 deletions

View File

@ -0,0 +1,296 @@
// crates/smithy-core/generator.rs
use std::{collections::HashMap, fs, path::Path};
use crate::types::{self as types, SmithyModel};
/// Generator for creating Smithy IDL files from models
pub struct SmithyGenerator {
models: Vec<SmithyModel>,
}
impl SmithyGenerator {
pub fn new() -> Self {
Self { models: Vec::new() }
}
pub fn add_model(&mut self, model: SmithyModel) {
self.models.push(model);
}
pub fn generate_idl(&self, output_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
fs::create_dir_all(output_dir)?;
let mut namespace_models: HashMap<String, Vec<&SmithyModel>> = HashMap::new();
let mut shape_to_namespace: HashMap<String, String> = HashMap::new();
// First, build a map of all shape names to their namespaces
for model in &self.models {
for shape_name in model.shapes.keys() {
shape_to_namespace.insert(shape_name.clone(), model.namespace.clone());
}
}
// Group models by namespace for file generation
for model in &self.models {
namespace_models
.entry(model.namespace.clone())
.or_default()
.push(model);
}
for (namespace, models) in namespace_models {
let filename = format!("{}.smithy", namespace.replace('.', "_"));
let filepath = output_dir.join(filename);
let mut content = String::new();
content.push_str("$version: \"2\"\n\n");
content.push_str(&format!("namespace {}\n\n", namespace));
// Collect all unique shape definitions for the current namespace
let mut shapes_in_namespace = HashMap::new();
for model in models {
for (shape_name, shape) in &model.shapes {
shapes_in_namespace.insert(shape_name.clone(), shape.clone());
}
}
// Generate definitions for each shape in the namespace
for (shape_name, shape) in &shapes_in_namespace {
content.push_str(&self.generate_shape_definition(
shape_name,
shape,
&namespace,
&shape_to_namespace,
));
content.push_str("\n\n");
}
fs::write(filepath, content)?;
}
Ok(())
}
fn generate_shape_definition(
&self,
name: &str,
shape: &types::SmithyShape,
current_namespace: &str,
shape_to_namespace: &HashMap<String, String>,
) -> String {
let resolve_target =
|target: &str| self.resolve_type(target, current_namespace, shape_to_namespace);
match shape {
types::SmithyShape::Structure {
members,
documentation,
traits,
} => {
let mut def = String::new();
if let Some(doc) = documentation {
def.push_str(&format!("/// {}\n", doc));
}
for smithy_trait in traits {
def.push_str(&format!("@{}\n", self.trait_to_string(smithy_trait)));
}
def.push_str(&format!("structure {} {{\n", name));
for (member_name, member) in members {
if let Some(doc) = &member.documentation {
def.push_str(&format!(" /// {}\n", doc));
}
for smithy_trait in &member.traits {
def.push_str(&format!(" @{}\n", self.trait_to_string(smithy_trait)));
}
let resolved_target = resolve_target(&member.target);
def.push_str(&format!(" {}: {}\n", member_name, resolved_target));
}
def.push('}');
def
}
types::SmithyShape::Union {
members,
documentation,
traits,
} => {
let mut def = String::new();
if let Some(doc) = documentation {
def.push_str(&format!("/// {}\n", doc));
}
for smithy_trait in traits {
def.push_str(&format!("@{}\n", self.trait_to_string(smithy_trait)));
}
def.push_str(&format!("union {} {{\n", name));
for (member_name, member) in members {
if let Some(doc) = &member.documentation {
def.push_str(&format!(" /// {}\n", doc));
}
for smithy_trait in &member.traits {
def.push_str(&format!(" @{}\n", self.trait_to_string(smithy_trait)));
}
let resolved_target = resolve_target(&member.target);
def.push_str(&format!(" {}: {}\n", member_name, resolved_target));
}
def.push('}');
def
}
types::SmithyShape::Enum {
values,
documentation,
traits,
} => {
let mut def = String::new();
if let Some(doc) = documentation {
def.push_str(&format!("/// {}\n", doc));
}
for smithy_trait in traits {
def.push_str(&format!("@{}\n", self.trait_to_string(smithy_trait)));
}
def.push_str(&format!("enum {} {{\n", name));
for (value_name, enum_value) in values {
if let Some(doc) = &enum_value.documentation {
def.push_str(&format!(" /// {}\n", doc));
}
def.push_str(&format!(" {}\n", value_name));
}
def.push('}');
def
}
types::SmithyShape::String { traits } => {
let mut def = String::new();
for smithy_trait in traits {
def.push_str(&format!("@{}\n", self.trait_to_string(smithy_trait)));
}
def.push_str(&format!("string {}", name));
def
}
types::SmithyShape::Integer { traits } => {
let mut def = String::new();
for smithy_trait in traits {
def.push_str(&format!("@{}\n", self.trait_to_string(smithy_trait)));
}
def.push_str(&format!("integer {}", name));
def
}
types::SmithyShape::Long { traits } => {
let mut def = String::new();
for smithy_trait in traits {
def.push_str(&format!("@{}\n", self.trait_to_string(smithy_trait)));
}
def.push_str(&format!("long {}", name));
def
}
types::SmithyShape::Boolean { traits } => {
let mut def = String::new();
for smithy_trait in traits {
def.push_str(&format!("@{}\n", self.trait_to_string(smithy_trait)));
}
def.push_str(&format!("boolean {}", name));
def
}
types::SmithyShape::List { member, traits } => {
let mut def = String::new();
for smithy_trait in traits {
def.push_str(&format!("@{}\n", self.trait_to_string(smithy_trait)));
}
def.push_str(&format!("list {} {{\n", name));
let resolved_target = resolve_target(&member.target);
def.push_str(&format!(" member: {}\n", resolved_target));
def.push('}');
def
}
}
}
fn resolve_type(
&self,
target: &str,
current_namespace: &str,
shape_to_namespace: &HashMap<String, String>,
) -> String {
// If the target is a primitive or a fully qualified Smithy type, return it as is
if target.starts_with("smithy.api#") {
return target.to_string();
}
// If the target is a custom type, resolve its namespace
if let Some(target_namespace) = shape_to_namespace.get(target) {
if target_namespace == current_namespace {
// The type is in the same namespace, so no qualification is needed
target.to_string()
} else {
// The type is in a different namespace, so it needs to be fully qualified
format!("{}#{}", target_namespace, target)
}
} else {
// If the type is not found in the shape map, it might be a primitive
// or an unresolved type. For now, return it as is.
target.to_string()
}
}
fn trait_to_string(&self, smithy_trait: &types::SmithyTrait) -> String {
match smithy_trait {
types::SmithyTrait::Pattern { pattern } => {
format!("pattern(\"{}\")", pattern)
}
types::SmithyTrait::Range { min, max } => match (min, max) {
(Some(min), Some(max)) => format!("range(min: {}, max: {})", min, max),
(Some(min), None) => format!("range(min: {})", min),
(None, Some(max)) => format!("range(max: {})", max),
(None, None) => "range".to_string(),
},
types::SmithyTrait::Required => "required".to_string(),
types::SmithyTrait::Documentation { documentation } => {
format!("documentation(\"{}\")", documentation)
}
types::SmithyTrait::Length { min, max } => match (min, max) {
(Some(min), Some(max)) => format!("length(min: {}, max: {})", min, max),
(Some(min), None) => format!("length(min: {})", min),
(None, Some(max)) => format!("length(max: {})", max),
(None, None) => "length".to_string(),
},
types::SmithyTrait::HttpLabel => "httpLabel".to_string(),
types::SmithyTrait::HttpQuery { name } => {
format!("httpQuery(\"{}\")", name)
}
types::SmithyTrait::Mixin => "mixin".to_string(),
}
}
}
impl Default for SmithyGenerator {
fn default() -> Self {
Self::new()
}
}

View File

@ -0,0 +1,7 @@
// // crates/smithy-core/lib.rs
pub mod generator;
pub mod types;
pub use generator::SmithyGenerator;
pub use types::*;

View File

@ -0,0 +1,331 @@
// crates/smithy-core/types.rs
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SmithyModel {
pub namespace: String,
pub shapes: HashMap<String, SmithyShape>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum SmithyShape {
#[serde(rename = "structure")]
Structure {
members: HashMap<String, SmithyMember>,
#[serde(skip_serializing_if = "Option::is_none")]
documentation: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
traits: Vec<SmithyTrait>,
},
#[serde(rename = "string")]
String {
#[serde(skip_serializing_if = "Vec::is_empty")]
traits: Vec<SmithyTrait>,
},
#[serde(rename = "integer")]
Integer {
#[serde(skip_serializing_if = "Vec::is_empty")]
traits: Vec<SmithyTrait>,
},
#[serde(rename = "long")]
Long {
#[serde(skip_serializing_if = "Vec::is_empty")]
traits: Vec<SmithyTrait>,
},
#[serde(rename = "boolean")]
Boolean {
#[serde(skip_serializing_if = "Vec::is_empty")]
traits: Vec<SmithyTrait>,
},
#[serde(rename = "list")]
List {
member: Box<SmithyMember>,
#[serde(skip_serializing_if = "Vec::is_empty")]
traits: Vec<SmithyTrait>,
},
#[serde(rename = "union")]
Union {
members: HashMap<String, SmithyMember>,
#[serde(skip_serializing_if = "Option::is_none")]
documentation: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
traits: Vec<SmithyTrait>,
},
#[serde(rename = "enum")]
Enum {
values: HashMap<String, SmithyEnumValue>,
#[serde(skip_serializing_if = "Option::is_none")]
documentation: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
traits: Vec<SmithyTrait>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SmithyMember {
pub target: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub traits: Vec<SmithyTrait>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SmithyEnumValue {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation: Option<String>,
pub is_default: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "trait")]
pub enum SmithyTrait {
#[serde(rename = "smithy.api#pattern")]
Pattern { pattern: String },
#[serde(rename = "smithy.api#range")]
Range { min: Option<i64>, max: Option<i64> },
#[serde(rename = "smithy.api#required")]
Required,
#[serde(rename = "smithy.api#documentation")]
Documentation { documentation: String },
#[serde(rename = "smithy.api#length")]
Length { min: Option<u64>, max: Option<u64> },
#[serde(rename = "smithy.api#httpLabel")]
HttpLabel,
#[serde(rename = "smithy.api#httpQuery")]
HttpQuery { name: String },
#[serde(rename = "smithy.api#mixin")]
Mixin,
}
#[derive(Debug, Clone)]
pub struct SmithyField {
pub name: String,
pub value_type: String,
pub constraints: Vec<SmithyConstraint>,
pub documentation: Option<String>,
pub optional: bool,
pub flatten: bool,
}
#[derive(Debug, Clone)]
pub struct SmithyEnumVariant {
pub name: String,
pub fields: Vec<SmithyField>,
pub constraints: Vec<SmithyConstraint>,
pub documentation: Option<String>,
}
#[derive(Debug, Clone)]
pub enum SmithyConstraint {
Pattern(String),
Range(Option<i64>, Option<i64>),
Length(Option<u64>, Option<u64>),
Required,
HttpLabel,
HttpQuery(String),
}
pub trait SmithyModelGenerator {
fn generate_smithy_model() -> SmithyModel;
}
// Helper functions moved from the proc-macro crate to be accessible by it.
pub fn resolve_type_and_generate_shapes(
value_type: &str,
shapes: &mut HashMap<String, SmithyShape>,
) -> Result<(String, HashMap<String, SmithyShape>), syn::Error> {
let value_type = value_type.trim();
let value_type_span = proc_macro2::Span::call_site();
let mut generated_shapes = HashMap::new();
let target_type = match value_type {
"String" | "str" => "smithy.api#String".to_string(),
"i8" | "i16" | "i32" | "u8" | "u16" | "u32" => "smithy.api#Integer".to_string(),
"i64" | "u64" | "isize" | "usize" => "smithy.api#Long".to_string(),
"f32" => "smithy.api#Float".to_string(),
"f64" => "smithy.api#Double".to_string(),
"bool" => "smithy.api#Boolean".to_string(),
"PrimitiveDateTime" | "time::PrimitiveDateTime" => "smithy.api#Timestamp".to_string(),
"Amount" | "MinorUnit" => "smithy.api#Long".to_string(),
"serde_json::Value" | "Value" | "Object" => "smithy.api#Document".to_string(),
"Url" | "url::Url" => "smithy.api#String".to_string(),
vt if vt.starts_with("Option<") && vt.ends_with('>') => {
let inner_type = extract_generic_inner_type(vt, "Option")
.map_err(|e| syn::Error::new(value_type_span, e))?;
let (resolved_type, new_shapes) = resolve_type_and_generate_shapes(inner_type, shapes)?;
generated_shapes.extend(new_shapes);
resolved_type
}
vt if vt.starts_with("Vec<") && vt.ends_with('>') => {
let inner_type = extract_generic_inner_type(vt, "Vec")
.map_err(|e| syn::Error::new(value_type_span, e))?;
let (inner_smithy_type, new_shapes) =
resolve_type_and_generate_shapes(inner_type, shapes)?;
generated_shapes.extend(new_shapes);
let list_shape_name = format!(
"{}List",
inner_smithy_type
.split("::")
.last()
.unwrap_or(&inner_smithy_type)
.split('#')
.next_back()
.unwrap_or(&inner_smithy_type)
);
if !shapes.contains_key(&list_shape_name)
&& !generated_shapes.contains_key(&list_shape_name)
{
let list_shape = SmithyShape::List {
member: Box::new(SmithyMember {
target: inner_smithy_type,
documentation: None,
traits: vec![],
}),
traits: vec![],
};
generated_shapes.insert(list_shape_name.clone(), list_shape);
}
list_shape_name
}
vt if vt.starts_with("Box<") && vt.ends_with('>') => {
let inner_type = extract_generic_inner_type(vt, "Box")
.map_err(|e| syn::Error::new(value_type_span, e))?;
let (resolved_type, new_shapes) = resolve_type_and_generate_shapes(inner_type, shapes)?;
generated_shapes.extend(new_shapes);
resolved_type
}
vt if vt.starts_with("Secret<") && vt.ends_with('>') => {
let inner_type = extract_generic_inner_type(vt, "Secret")
.map_err(|e| syn::Error::new(value_type_span, e))?;
let (resolved_type, new_shapes) = resolve_type_and_generate_shapes(inner_type, shapes)?;
generated_shapes.extend(new_shapes);
resolved_type
}
vt if vt.starts_with("HashMap<") && vt.ends_with('>') => {
let inner_types = extract_generic_inner_type(vt, "HashMap")
.map_err(|e| syn::Error::new(value_type_span, e))?;
let (key_type, value_type) =
parse_map_types(inner_types).map_err(|e| syn::Error::new(value_type_span, e))?;
let (key_smithy_type, key_shapes) = resolve_type_and_generate_shapes(key_type, shapes)?;
generated_shapes.extend(key_shapes);
let (value_smithy_type, value_shapes) =
resolve_type_and_generate_shapes(value_type, shapes)?;
generated_shapes.extend(value_shapes);
format!(
"smithy.api#Map<key: {}, value: {}>",
key_smithy_type, value_smithy_type
)
}
vt if vt.starts_with("BTreeMap<") && vt.ends_with('>') => {
let inner_types = extract_generic_inner_type(vt, "BTreeMap")
.map_err(|e| syn::Error::new(value_type_span, e))?;
let (key_type, value_type) =
parse_map_types(inner_types).map_err(|e| syn::Error::new(value_type_span, e))?;
let (key_smithy_type, key_shapes) = resolve_type_and_generate_shapes(key_type, shapes)?;
generated_shapes.extend(key_shapes);
let (value_smithy_type, value_shapes) =
resolve_type_and_generate_shapes(value_type, shapes)?;
generated_shapes.extend(value_shapes);
format!(
"smithy.api#Map<key: {}, value: {}>",
key_smithy_type, value_smithy_type
)
}
_ => {
if value_type.contains("::") {
value_type.replace("::", ".")
} else {
value_type.to_string()
}
}
};
Ok((target_type, generated_shapes))
}
fn extract_generic_inner_type<'a>(full_type: &'a str, wrapper: &str) -> Result<&'a str, String> {
let expected_start = format!("{}<", wrapper);
if !full_type.starts_with(&expected_start) || !full_type.ends_with('>') {
return Err(format!("Invalid {} type format: {}", wrapper, full_type));
}
let start_idx = expected_start.len();
let end_idx = full_type.len() - 1;
if start_idx >= end_idx {
return Err(format!("Empty {} type: {}", wrapper, full_type));
}
if start_idx >= full_type.len() || end_idx > full_type.len() {
return Err(format!(
"Invalid index bounds for {} type: {}",
wrapper, full_type
));
}
Ok(full_type
.get(start_idx..end_idx)
.ok_or_else(|| {
format!(
"Failed to extract inner type from {}: {}",
wrapper, full_type
)
})?
.trim())
}
fn parse_map_types(inner_types: &str) -> Result<(&str, &str), String> {
// Handle nested generics by counting angle brackets
let mut bracket_count = 0;
let mut comma_pos = None;
for (i, ch) in inner_types.char_indices() {
match ch {
'<' => bracket_count += 1,
'>' => bracket_count -= 1,
',' if bracket_count == 0 => {
comma_pos = Some(i);
break;
}
_ => {}
}
}
if let Some(pos) = comma_pos {
let key_type = inner_types
.get(..pos)
.ok_or_else(|| format!("Invalid key type bounds in map: {}", inner_types))?
.trim();
let value_type = inner_types
.get(pos + 1..)
.ok_or_else(|| format!("Invalid value type bounds in map: {}", inner_types))?
.trim();
if key_type.is_empty() || value_type.is_empty() {
return Err(format!("Invalid map type format: {}", inner_types));
}
Ok((key_type, value_type))
} else {
Err(format!(
"Invalid map type format, missing comma: {}",
inner_types
))
}
}