mod helpers;
mod python_service;
mod registration_variants;
mod rust_service;
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::ApiSurface;
use std::path::PathBuf;
pub(super) fn gen_service_py(api: &ApiSurface, module_name: &str) -> String {
python_service::gen_service_py(api, module_name)
}
pub(super) fn gen_service_rs(api: &ApiSurface, config: &ResolvedCrateConfig) -> String {
rust_service::gen_service_rs(api, config)
}
pub fn generate(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
if api.services.is_empty() {
return Ok(vec![]);
}
use crate::core::config::resolve_output_dir;
let output_dir = resolve_output_dir(config.output_paths.get("python"), &config.name, "crates/{name}-py/src/");
let module_name = config.python_module_name();
let service_rs = gen_service_rs(api, config);
let service_py = gen_service_py(api, &module_name);
let output_base = config
.python
.as_ref()
.and_then(|p| p.stubs.as_ref())
.map(|s| PathBuf::from(&s.output))
.unwrap_or_else(|| {
let package_name = config.name.replace('-', "_");
PathBuf::from(format!("packages/python/{}", package_name))
});
Ok(vec![
GeneratedFile {
path: PathBuf::from(&output_dir).join("service.rs"),
content: service_rs,
generated_header: true,
},
GeneratedFile {
path: output_base.join("service.py"),
content: service_py,
generated_header: true,
},
])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{
EntrypointDef, EntrypointKind, HandlerContractDef, MethodDef, ParamDef, PrimitiveType, RegistrationDef,
ServiceDef, TypeRef,
};
fn make_fixture_surface() -> ApiSurface {
let constructor = MethodDef {
name: "new".to_owned(),
params: vec![],
return_type: TypeRef::Unit,
is_async: false,
is_static: true,
error_type: None,
doc: "Create a new service owner.".to_owned(),
receiver: None,
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
};
let configurator = MethodDef {
name: "with_timeout".to_owned(),
params: vec![ParamDef {
name: "timeout_ms".to_owned(),
ty: TypeRef::Primitive(PrimitiveType::U64),
optional: false,
default: None,
..ParamDef::default()
}],
return_type: TypeRef::Named("TestService".to_owned()),
is_async: false,
is_static: false,
error_type: None,
doc: "Set request timeout.".to_owned(),
receiver: Some(crate::core::ir::ReceiverKind::RefMut),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
};
let registration = RegistrationDef {
method: "add_handler".to_owned(),
callback_param: "handler".to_owned(),
callback_contract: "RequestHandler".to_owned(),
metadata_params: vec![
ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
},
ParamDef {
name: "method".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
},
],
receiver: Some(crate::core::ir::ReceiverKind::RefMut),
return_type: TypeRef::Unit,
error_type: None,
doc: "Register a request handler for a path and method.".to_owned(),
variants: vec![],
..Default::default()
};
let run_ep = EntrypointDef {
method: "run".to_owned(),
kind: EntrypointKind::Run,
is_async: true,
params: vec![ParamDef {
name: "addr".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
}],
return_type: TypeRef::Unit,
error_type: Some("ServiceError".to_owned()),
doc: "Run the service.".to_owned(),
};
let finalize_ep = EntrypointDef {
method: "into_router".to_owned(),
kind: EntrypointKind::Finalize,
is_async: false,
params: vec![],
return_type: TypeRef::Named("Router".to_owned()),
error_type: None,
doc: "Consume and convert into a router.".to_owned(),
};
let service = ServiceDef {
name: "TestService".to_owned(),
rust_path: "my_crate::TestService".to_owned(),
constructor,
configurators: vec![configurator],
registrations: vec![registration],
entrypoints: vec![run_ep, finalize_ep],
doc: "A test service owner.".to_owned(),
cfg: None,
};
let dispatch_method = MethodDef {
name: "handle".to_owned(),
params: vec![ParamDef {
name: "request".to_owned(),
ty: TypeRef::Named("RequestData".to_owned()),
optional: false,
default: None,
..ParamDef::default()
}],
return_type: TypeRef::Named("ResponseData".to_owned()),
is_async: true,
is_static: false,
error_type: Some("HandlerError".to_owned()),
doc: "Dispatch a request.".to_owned(),
receiver: Some(crate::core::ir::ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
};
let contract = HandlerContractDef {
trait_name: "RequestHandler".to_owned(),
rust_path: "my_crate::RequestHandler".to_owned(),
dispatch: dispatch_method,
optional_methods: vec![],
wire_request_type: Some("RequestData".to_owned()),
wire_response_type: Some("ResponseData".to_owned()),
dispatch_extra_params: vec![],
wire_param_name: None,
dispatch_return_type: None,
response_adapter: None,
doc: "Async trait for handling requests.".to_owned(),
};
ApiSurface {
crate_name: "my_crate".to_owned(),
version: "0.1.0".to_owned(),
services: vec![service],
handler_contracts: vec![contract],
..ApiSurface::default()
}
}
#[test]
fn python_output_contains_service_class() {
let surface = make_fixture_surface();
let output = gen_service_py(&surface, "_my_crate");
assert!(
output.contains("class TestService:"),
"expected `class TestService:` in output:\n{output}"
);
}
#[test]
fn python_output_contains_init_with_registrations() {
let surface = make_fixture_surface();
let output = gen_service_py(&surface, "_my_crate");
assert!(
output.contains("def __init__(self)"),
"expected `def __init__(self)` in output:\n{output}"
);
assert!(
output.contains("self._registrations"),
"expected `self._registrations` in output:\n{output}"
);
}
#[test]
fn python_output_contains_configurator() {
let surface = make_fixture_surface();
let output = gen_service_py(&surface, "_my_crate");
assert!(
output.contains("def with_timeout(self, timeout_ms: int)"),
"expected `with_timeout` configurator:\n{output}"
);
assert!(
output.contains("return self"),
"expected `return self` in configurator:\n{output}"
);
}
#[test]
fn python_output_contains_registration_decorator() {
let surface = make_fixture_surface();
let output = gen_service_py(&surface, "_my_crate");
assert!(
output.contains("def add_handler("),
"expected `add_handler` registration method:\n{output}"
);
assert!(
output.contains("def _decorator(fn"),
"expected inner `_decorator` closure:\n{output}"
);
assert!(
output.contains("self._registrations.append"),
"expected `_registrations.append` in decorator:\n{output}"
);
}
#[test]
fn python_output_contains_run_entrypoint() {
let surface = make_fixture_surface();
let output = gen_service_py(&surface, "_my_crate");
assert!(
output.contains("def run(self"),
"expected `def run(self` entrypoint:\n{output}"
);
assert!(
output.contains("_my_crate.test_service_run("),
"expected native call `_my_crate.test_service_run(` in run:\n{output}"
);
}
#[test]
fn python_output_contains_registration_variants() {
let mut surface = make_fixture_surface();
let variant = crate::core::ir::RegistrationVariant {
name: "get".to_owned(),
overrides: vec![],
wrapper_call: Some(crate::core::ir::WrapperConstructorCall {
metadata_param: "builder".to_owned(),
wrapper_type_path: "mylib::RouteBuilder".to_owned(),
wrapper_type_name: "RouteBuilder".to_owned(),
constructor_method: "new".to_owned(),
args: vec![
crate::core::ir::WrapperConstructorArg::Fixed {
param_name: "method".to_owned(),
value_expr: "mylib::Method::GET".to_owned(),
},
crate::core::ir::WrapperConstructorArg::Free {
param: ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
},
},
],
}),
signature_params: vec![ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
}],
doc: Some("Register a GET handler.".to_owned()),
style: crate::core::ir::RegistrationVariantStyle::Hybrid,
..Default::default()
};
surface.services[0].registrations[0].variants.push(variant);
let output = gen_service_py(&surface, "_my_crate");
assert!(
output.contains("def get(self, path: str, handler: Callable[..., Any])"),
"expected `def get(self, path: str, handler)` method form:\n{output}"
);
assert!(
output.contains("def get_decorator(self, path: str)"),
"expected `def get_decorator(self, path: str)` decorator form:\n{output}"
);
assert!(
output.contains("builder = RouteBuilder.new(Method.GET, path)"),
"expected wrapper constructor call with Method.GET:\n{output}"
);
assert!(
output.contains("(\"add_handler\", (builder,), handler)"),
"expected metadata tuple to contain only the constructed wrapper:\n{output}"
);
}
#[test]
fn rust_output_contains_handler_bridge_struct() {
let surface = make_fixture_surface();
let config = make_test_config();
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("pub struct PyRequestHandlerBridge"),
"expected `PyRequestHandlerBridge` struct:\n{output}"
);
}
#[test]
fn rust_output_contains_handler_bridge_impl() {
let surface = make_fixture_surface();
let config = make_test_config();
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("impl my_crate::RequestHandler for PyRequestHandlerBridge"),
"expected trait impl:\n{output}"
);
assert!(
output.contains("fn handle(") && output.contains("Pin<Box<dyn Future<Output"),
"expected dispatch method returning a boxed future:\n{output}"
);
}
#[test]
fn rust_output_contains_pyfunction_run() {
let surface = make_fixture_surface();
let config = make_test_config();
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("#[pyfunction]"),
"expected `#[pyfunction]` attribute:\n{output}"
);
assert!(
output.contains("pub fn test_service_run("),
"expected `test_service_run` function:\n{output}"
);
}
#[test]
fn rust_output_contains_registration_dispatch() {
let surface = make_fixture_surface();
let config = make_test_config();
let output = gen_service_rs(&surface, &config);
assert!(
output.contains("\"add_handler\""),
"expected `\"add_handler\"` match arm:\n{output}"
);
assert!(
output.contains("Arc<dyn my_crate::RequestHandler>"),
"expected Arc wrapping of handler:\n{output}"
);
}
#[test]
fn generate_returns_two_files_for_non_empty_services() {
let surface = make_fixture_surface();
let config = make_test_config();
let files = generate(&surface, &config).expect("generate should not fail");
assert_eq!(files.len(), 2, "expected 2 generated files, got {}", files.len());
let paths: Vec<&str> = files
.iter()
.map(|f| f.path.file_name().unwrap().to_str().unwrap())
.collect();
assert!(paths.contains(&"service.rs"), "expected service.rs in output");
assert!(paths.contains(&"service.py"), "expected service.py in output");
}
#[test]
fn generate_returns_empty_for_no_services() {
let surface = ApiSurface::default();
let config = make_test_config();
let files = generate(&surface, &config).expect("generate should not fail");
assert!(files.is_empty(), "expected no files for surface without services");
}
fn make_test_config() -> ResolvedCrateConfig {
use crate::core::config::resolved::ResolvedCrateConfig;
ResolvedCrateConfig {
name: "my-crate".to_owned(),
..ResolvedCrateConfig::default()
}
}
}