use serde::{Deserialize, Serialize};
use super::items::{MethodDef, ParamDef, ReceiverKind};
use super::type_ref::TypeRef;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceDef {
pub name: String,
pub rust_path: String,
pub constructor: MethodDef,
pub configurators: Vec<MethodDef>,
pub registrations: Vec<RegistrationDef>,
pub entrypoints: Vec<EntrypointDef>,
pub doc: String,
pub cfg: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct ParameterConstraint {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub type_constraint: Option<String>,
}
impl ParameterConstraint {
pub fn parse_parameters(path: &str) -> (String, Vec<ParameterConstraint>) {
let mut constraints = Vec::new();
let mut normalized = String::with_capacity(path.len());
let mut chars = path.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '{' {
let mut inner = String::new();
for c in chars.by_ref() {
if c == '}' {
break;
}
inner.push(c);
}
let (name, type_constraint) = if let Some(colon_pos) = inner.find(':') {
let n = inner[..colon_pos].trim().to_owned();
let t = inner[colon_pos + 1..].trim().to_owned();
(n, if t.is_empty() { None } else { Some(t) })
} else {
(inner.trim().to_owned(), None)
};
if !name.is_empty() {
constraints.push(ParameterConstraint {
name: name.clone(),
type_constraint,
});
normalized.push('{');
normalized.push_str(&name);
normalized.push('}');
}
} else if ch == ':' && chars.peek().is_some_and(|c| c.is_alphabetic() || *c == '_') {
let mut name = String::new();
while chars.peek().is_some_and(|c| c.is_alphanumeric() || *c == '_') {
name.push(chars.next().unwrap());
}
if !name.is_empty() {
constraints.push(ParameterConstraint {
name: name.clone(),
type_constraint: None,
});
normalized.push('{');
normalized.push_str(&name);
normalized.push('}');
}
} else {
normalized.push(ch);
}
}
(normalized, constraints)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum HandlerShape {
#[default]
BareCallable,
ContextObject,
RequestResponse,
IntrospectParams,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RegistrationDef {
pub method: String,
pub callback_param: String,
pub callback_contract: String,
pub metadata_params: Vec<ParamDef>,
pub receiver: Option<ReceiverKind>,
pub return_type: TypeRef,
pub error_type: Option<String>,
pub doc: String,
#[serde(default)]
pub variants: Vec<RegistrationVariant>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub path_param_constraints: Vec<ParameterConstraint>,
#[serde(default)]
pub handler_shape: HandlerShape,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum RegistrationVariantStyle {
Builder,
VerbDecorator,
#[default]
Hybrid,
Decorator,
Attribute,
Dsl,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct RegistrationVariantLanguageOverride {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub style: Option<RegistrationVariantStyle>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub handler_shape: Option<HandlerShape>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub method_prefix: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedVariant<'a> {
pub style: RegistrationVariantStyle,
pub handler_shape: HandlerShape,
pub method_prefix: Option<&'a str>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct RegistrationVariant {
pub name: String,
pub overrides: Vec<RegistrationVariantOverride>,
#[serde(default)]
pub wrapper_call: Option<WrapperConstructorCall>,
pub signature_params: Vec<ParamDef>,
#[serde(default)]
pub doc: Option<String>,
#[serde(default)]
pub style: RegistrationVariantStyle,
#[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
pub language_overrides: std::collections::HashMap<String, RegistrationVariantLanguageOverride>,
}
impl RegistrationVariant {
pub fn resolved_for(&self, language: &str, base_handler_shape: HandlerShape) -> ResolvedVariant<'_> {
if let Some(lang_override) = self.language_overrides.get(language) {
ResolvedVariant {
style: lang_override.style.unwrap_or(self.style),
handler_shape: lang_override.handler_shape.unwrap_or(base_handler_shape),
method_prefix: lang_override.method_prefix.as_deref(),
}
} else {
ResolvedVariant {
style: self.style,
handler_shape: base_handler_shape,
method_prefix: None,
}
}
}
pub fn method_name_for<'a>(&'a self, language: &str) -> std::borrow::Cow<'a, str> {
if let Some(lang_override) = self.language_overrides.get(language) {
if let Some(prefix) = &lang_override.method_prefix {
return std::borrow::Cow::Owned(format!("{}{}", prefix, self.name));
}
}
std::borrow::Cow::Borrowed(&self.name)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistrationVariantOverride {
pub param_name: String,
pub value_expr: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WrapperConstructorCall {
pub metadata_param: String,
pub wrapper_type_path: String,
pub wrapper_type_name: String,
pub constructor_method: String,
pub args: Vec<WrapperConstructorArg>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum WrapperConstructorArg {
Fixed {
param_name: String,
value_expr: String,
},
Free {
param: ParamDef,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntrypointDef {
pub method: String,
pub kind: EntrypointKind,
pub is_async: bool,
pub params: Vec<ParamDef>,
pub return_type: TypeRef,
pub error_type: Option<String>,
pub doc: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EntrypointKind {
Run,
Finalize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HandlerContractDef {
pub trait_name: String,
pub rust_path: String,
pub dispatch: MethodDef,
pub optional_methods: Vec<MethodDef>,
pub wire_request_type: Option<String>,
pub wire_response_type: Option<String>,
pub dispatch_extra_params: Vec<String>,
pub wire_param_name: Option<String>,
pub dispatch_return_type: Option<String>,
pub response_adapter: Option<String>,
pub doc: String,
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn make_variant(style: RegistrationVariantStyle) -> RegistrationVariant {
RegistrationVariant {
name: "get".to_owned(),
style,
..RegistrationVariant::default()
}
}
#[test]
fn resolved_for_no_override_returns_variant_defaults() {
let variant = make_variant(RegistrationVariantStyle::VerbDecorator);
let resolved = variant.resolved_for("python", HandlerShape::BareCallable);
assert_eq!(resolved.style, RegistrationVariantStyle::VerbDecorator);
assert_eq!(resolved.handler_shape, HandlerShape::BareCallable);
assert_eq!(resolved.method_prefix, None);
}
#[test]
fn resolved_for_no_override_propagates_base_handler_shape() {
let variant = make_variant(RegistrationVariantStyle::Hybrid);
let resolved = variant.resolved_for("napi", HandlerShape::ContextObject);
assert_eq!(resolved.handler_shape, HandlerShape::ContextObject);
assert_eq!(resolved.style, RegistrationVariantStyle::Hybrid);
}
#[test]
fn resolved_for_language_override_wins_over_variant_style() {
let mut overrides = HashMap::new();
overrides.insert(
"csharp".to_owned(),
RegistrationVariantLanguageOverride {
style: Some(RegistrationVariantStyle::Attribute),
handler_shape: None,
method_prefix: Some("Map".to_owned()),
},
);
let variant = RegistrationVariant {
name: "get".to_owned(),
style: RegistrationVariantStyle::Hybrid,
language_overrides: overrides,
..RegistrationVariant::default()
};
let resolved = variant.resolved_for("csharp", HandlerShape::BareCallable);
assert_eq!(
resolved.style,
RegistrationVariantStyle::Attribute,
"override style should win"
);
assert_eq!(
resolved.handler_shape,
HandlerShape::BareCallable,
"base shape should be kept"
);
assert_eq!(resolved.method_prefix, Some("Map"), "prefix from override");
}
#[test]
fn resolved_for_language_override_handler_shape_wins() {
let mut overrides = HashMap::new();
overrides.insert(
"kotlin".to_owned(),
RegistrationVariantLanguageOverride {
style: Some(RegistrationVariantStyle::Dsl),
handler_shape: Some(HandlerShape::ContextObject),
method_prefix: None,
},
);
let variant = RegistrationVariant {
name: "get".to_owned(),
style: RegistrationVariantStyle::Builder,
language_overrides: overrides,
..RegistrationVariant::default()
};
let resolved = variant.resolved_for("kotlin", HandlerShape::BareCallable);
assert_eq!(resolved.style, RegistrationVariantStyle::Dsl);
assert_eq!(
resolved.handler_shape,
HandlerShape::ContextObject,
"override shape should win"
);
assert_eq!(resolved.method_prefix, None);
}
#[test]
fn resolved_for_unrelated_language_falls_back_to_defaults() {
let mut overrides = HashMap::new();
overrides.insert(
"csharp".to_owned(),
RegistrationVariantLanguageOverride {
style: Some(RegistrationVariantStyle::Attribute),
handler_shape: Some(HandlerShape::ContextObject),
method_prefix: Some("Map".to_owned()),
},
);
let variant = RegistrationVariant {
name: "get".to_owned(),
style: RegistrationVariantStyle::Hybrid,
language_overrides: overrides,
..RegistrationVariant::default()
};
let resolved = variant.resolved_for("python", HandlerShape::IntrospectParams);
assert_eq!(resolved.style, RegistrationVariantStyle::Hybrid);
assert_eq!(resolved.handler_shape, HandlerShape::IntrospectParams);
assert_eq!(resolved.method_prefix, None);
}
#[test]
fn resolved_for_partial_override_only_style_no_shape() {
let mut overrides = HashMap::new();
overrides.insert(
"python".to_owned(),
RegistrationVariantLanguageOverride {
style: Some(RegistrationVariantStyle::Decorator),
handler_shape: None,
method_prefix: None,
},
);
let variant = RegistrationVariant {
name: "get".to_owned(),
style: RegistrationVariantStyle::Hybrid,
language_overrides: overrides,
..RegistrationVariant::default()
};
let resolved = variant.resolved_for("python", HandlerShape::RequestResponse);
assert_eq!(
resolved.style,
RegistrationVariantStyle::Decorator,
"style override applies"
);
assert_eq!(
resolved.handler_shape,
HandlerShape::RequestResponse,
"shape falls back to base"
);
}
#[test]
fn method_name_for_no_override_returns_variant_name() {
let variant = make_variant(RegistrationVariantStyle::Hybrid);
assert_eq!(variant.method_name_for("go").as_ref(), "get");
}
#[test]
fn method_name_for_prefix_override_prepends_prefix() {
let mut overrides = HashMap::new();
overrides.insert(
"csharp".to_owned(),
RegistrationVariantLanguageOverride {
style: None,
handler_shape: None,
method_prefix: Some("Map".to_owned()),
},
);
let variant = RegistrationVariant {
name: "get".to_owned(),
style: RegistrationVariantStyle::Hybrid,
language_overrides: overrides,
..RegistrationVariant::default()
};
assert_eq!(variant.method_name_for("csharp").as_ref(), "Mapget");
}
}