use std::path::{Path, PathBuf};
use olai_codegen::parsing::parse_file_descriptor_set;
use olai_codegen::{BindingsConfig, CodeGenConfig, CodeGenMetadata, CodeGenOutput, generate_code};
use protobuf::Message;
use protobuf::descriptor::FileDescriptorSet;
use tempfile::TempDir;
fn metadata() -> CodeGenMetadata {
let bytes = include_bytes!("../proto/example.bin");
let fds = FileDescriptorSet::parse_from_bytes(bytes).expect("valid descriptor");
parse_file_descriptor_set(&fds).expect("parse succeeds")
}
fn walk(dir: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
if let Ok(rd) = std::fs::read_dir(dir) {
for e in rd.flatten() {
let p = e.path();
if p.is_dir() {
files.extend(walk(&p));
} else {
files.push(p);
}
}
}
files
}
fn read_all(dir: &Path) -> String {
walk(dir)
.iter()
.filter_map(|p| std::fs::read_to_string(p).ok())
.collect::<Vec<_>>()
.join("\n")
}
fn rust_config(tmp: &Path) -> CodeGenConfig {
let common = tmp.join("common");
let models = tmp.join("models");
let server = tmp.join("server");
let client = tmp.join("client");
for d in [&common, &models, &server, &client] {
std::fs::create_dir_all(d).expect("create dir");
}
CodeGenConfig {
context_type_path: "crate::Context".into(),
result_type_path: "crate::Result".into(),
models_path_template: "example_common::models::{service}::v1".into(),
models_path_crate_template: "crate::models::{service}::v1".into(),
resource_store_crate_name: "olai_store".into(),
runtime: olai_codegen::Runtime::Prost,
transport_type_path: olai_codegen::DEFAULT_TRANSPORT_TYPE_PATH.into(),
dual_transport: false,
client_protocols: olai_codegen::ClientProtocols::default(),
connect_client_path: None,
rest_client: Default::default(),
output: CodeGenOutput {
common: Some(common),
models: Some(models),
models_subdir: "_gen".into(),
server: Some(server),
client: Some(client),
python: None,
node: None,
node_ts: None,
wasm: None,
generate_resource_clients: false,
},
generate_resource_enum: true,
generate_store_integration: true,
error_type_path: Some("crate::Error".into()),
generate_object_conversions: false,
bindings: None,
}
}
#[test]
fn wasm_transport_is_substituted_into_generated_clients() {
let tmp = TempDir::new().unwrap();
let mut config = rust_config(tmp.path());
config.transport_type_path = "olai_http_wasm::WasmClient".into();
config.bindings = Some(BindingsConfig {
aggregate_client_name: "ExampleClient".into(),
client_crate_name: "example_client".into(),
py_error_type: "PyExampleError".into(),
py_result_type: "PyExampleResult".into(),
napi_error_ext_trait: "NapiErrorExt".into(),
ts_error_base_class: "ExampleError".into(),
ts_error_code_prefix: "EX".into(),
python: Default::default(),
});
generate_code(&metadata(), &config).expect("generation succeeds");
let client_src = read_all(&tmp.path().join("client"));
assert!(
client_src.contains("olai_http_wasm :: WasmClient")
|| client_src.contains("olai_http_wasm::WasmClient"),
"expected the WASM transport to be imported into generated client code"
);
assert!(
!client_src.contains("use olai_http :: CloudClient")
&& !client_src.contains("use olai_http::CloudClient"),
"CloudClient must not be imported when a WASM transport is configured"
);
assert!(
!client_src.contains("new_unauthenticated") && !client_src.contains("new_with_token"),
"cloud-only aggregate ctors must not be emitted for a non-CloudClient transport"
);
for path in walk(&tmp.path().join("client"))
.into_iter()
.filter(|p| p.extension().is_some_and(|e| e == "rs"))
{
let src = std::fs::read_to_string(&path).expect("read generated file");
syn::parse_file(&src)
.unwrap_or_else(|e| panic!("generated {} is invalid Rust: {e}", path.display()));
}
}
#[test]
fn default_transport_still_uses_cloud_client() {
let tmp = TempDir::new().unwrap();
let config = rust_config(tmp.path());
assert_eq!(config.transport_type_path, "olai_http::CloudClient");
generate_code(&metadata(), &config).expect("generation succeeds");
let client_src = read_all(&tmp.path().join("client"));
assert!(
client_src.contains("CloudClient"),
"default transport must still emit CloudClient"
);
assert!(
!client_src.contains("WasmClient"),
"default transport must not mention the WASM transport"
);
}
#[test]
fn wasm_bindings_and_dts_are_emitted() {
let tmp = TempDir::new().unwrap();
let common = tmp.path().join("common");
let client = tmp.path().join("client");
let wasm = tmp.path().join("wasm");
for d in [&common, &client, &wasm] {
std::fs::create_dir_all(d).expect("create dir");
}
let config = CodeGenConfig {
context_type_path: "crate::Context".into(),
result_type_path: "crate::Result".into(),
models_path_template: "example_common::models::{service}::v1".into(),
models_path_crate_template: "crate::models::{service}::v1".into(),
resource_store_crate_name: "olai_store".into(),
runtime: olai_codegen::Runtime::Buffa,
transport_type_path: "olai_http_wasm::WasmClient".into(),
dual_transport: false,
client_protocols: olai_codegen::ClientProtocols::default(),
connect_client_path: None,
rest_client: Default::default(),
output: CodeGenOutput {
common: Some(common),
models: None,
models_subdir: "_gen".into(),
server: None,
client: Some(client),
python: None,
node: None,
node_ts: None,
wasm: Some(wasm.clone()),
generate_resource_clients: false,
},
generate_resource_enum: false,
generate_store_integration: false,
error_type_path: None,
generate_object_conversions: false,
bindings: Some(BindingsConfig {
aggregate_client_name: "ExampleClient".into(),
client_crate_name: "example_client".into(),
py_error_type: "PyExampleError".into(),
py_result_type: "PyExampleResult".into(),
napi_error_ext_trait: "NapiErrorExt".into(),
ts_error_base_class: "ExampleError".into(),
ts_error_code_prefix: "EX".into(),
python: Default::default(),
}),
};
generate_code(&metadata(), &config).expect("generation succeeds");
let bindings_rs = std::fs::read_to_string(wasm.join("mod.rs")).expect("wasm mod.rs written");
syn::parse_file(&bindings_rs).expect("generated wasm mod.rs is valid Rust");
assert!(
bindings_rs.contains("wasm_bindgen"),
"missing #[wasm_bindgen]"
);
assert!(
bindings_rs.contains("olai_http_wasm") && bindings_rs.contains("WasmClient"),
"wasm bindings must use the WasmClient transport"
);
assert!(
bindings_rs.contains("serde_wasm_bindgen"),
"wasm bindings must marshal via serde-wasm-bindgen"
);
assert!(
bindings_rs.contains("WasmExampleClient")
&& bindings_rs.contains("js_class = \"ExampleClient\""),
"expected the aggregate wasm wrapper exposed to JS as ExampleClient"
);
assert!(
bindings_rs.contains("options: Option<JsValue>")
&& bindings_rs.contains("auth_token")
&& bindings_rs.contains("with_auth")
&& bindings_rs.contains("with_credentials"),
"wasm bindings constructor must support an optional auth-token / credentials options object"
);
let dts = std::fs::read_to_string(wasm.join("client.d.ts")).expect("client.d.ts written");
assert!(
dts.contains("export class ExampleClient"),
"d.ts must declare the aggregate class"
);
assert!(
dts.contains("constructor(baseUrl: string"),
"d.ts aggregate must take a base URL"
);
assert!(
dts.contains("options?:") && dts.contains("authToken?:"),
"d.ts aggregate constructor must document the optional auth options"
);
assert!(
dts.contains("): Promise<"),
"d.ts methods must return Promises"
);
}
#[test]
fn wasm_output_emits_dual_cfg_gated_transport() {
let tmp = TempDir::new().unwrap();
let common = tmp.path().join("common");
let client = tmp.path().join("client");
let wasm = tmp.path().join("wasm");
for d in [&common, &client, &wasm] {
std::fs::create_dir_all(d).expect("create dir");
}
let mut config = rust_config(tmp.path());
config.output.wasm = Some(wasm);
config.bindings = Some(BindingsConfig {
aggregate_client_name: "ExampleClient".into(),
client_crate_name: "example_client".into(),
py_error_type: "PyExampleError".into(),
py_result_type: "PyExampleResult".into(),
napi_error_ext_trait: "NapiErrorExt".into(),
ts_error_base_class: "ExampleError".into(),
ts_error_code_prefix: "EX".into(),
python: Default::default(),
});
generate_code(&metadata(), &config).expect("generation succeeds");
let client_src = read_all(&tmp.path().join("client"));
assert!(
client_src.contains("cfg(target_arch = \"wasm32\")")
|| client_src.contains("cfg(target_arch = \"wasm32\")"),
"dual-transport client must gate on target_arch"
);
assert!(
client_src.contains("CloudClient") && client_src.contains("WasmClient"),
"dual-transport client must reference both CloudClient and WasmClient"
);
assert!(
client_src.contains("new_with_token") && client_src.contains("new_in_browser"),
"expected native cloud ctors + the browser ctor"
);
for path in walk(&tmp.path().join("client"))
.into_iter()
.filter(|p| p.extension().is_some_and(|e| e == "rs"))
{
let src = std::fs::read_to_string(&path).expect("read generated file");
syn::parse_file(&src)
.unwrap_or_else(|e| panic!("generated {} is invalid Rust: {e}", path.display()));
}
}
#[test]
fn dual_transport_flag_emits_alias_without_wasm_bindings() {
let tmp = TempDir::new().unwrap();
let mut config = rust_config(tmp.path());
config.runtime = olai_codegen::Runtime::Buffa;
config.dual_transport = true;
assert!(
config.output.wasm.is_none(),
"no JS bindings output configured"
);
generate_code(&metadata(), &config).expect("generation succeeds");
let client_src = read_all(&tmp.path().join("client"));
assert!(
client_src.contains("cfg(target_arch = \"wasm32\")")
&& client_src.contains("CloudClient")
&& client_src.contains("WasmClient"),
"dual_transport must emit the cfg-gated CloudClient/WasmClient alias"
);
assert!(
!client_src.contains("wasm_bindgen"),
"dual_transport alone must not emit #[wasm_bindgen] bindings"
);
for path in walk(&tmp.path().join("client"))
.into_iter()
.filter(|p| p.extension().is_some_and(|e| e == "rs"))
{
let src = std::fs::read_to_string(&path).expect("read generated file");
syn::parse_file(&src)
.unwrap_or_else(|e| panic!("generated {} is invalid Rust: {e}", path.display()));
}
}
#[test]
fn all_generated_rust_files_parse() {
let tmp = TempDir::new().unwrap();
let mut config = rust_config(tmp.path());
config.generate_object_conversions = true;
generate_code(&metadata(), &config).expect("generation succeeds");
let rust_files: Vec<PathBuf> = ["common", "models", "server", "client"]
.iter()
.flat_map(|d| walk(&tmp.path().join(d)))
.filter(|p| p.extension().is_some_and(|e| e == "rs"))
.collect();
assert!(
!rust_files.is_empty(),
"expected generated .rs files, found none"
);
for path in &rust_files {
let src = std::fs::read_to_string(path).expect("read generated file");
if let Err(err) = syn::parse_file(&src) {
panic!(
"generated file {} is not valid Rust: {err}\n---\n{src}",
path.display()
);
}
}
}
#[test]
fn client_methods_build_urls_query_and_error_handling() {
let tmp = TempDir::new().unwrap();
let config = rust_config(tmp.path());
generate_code(&metadata(), &config).expect("generation succeeds");
let client = read_all(&tmp.path().join("client"));
assert!(
client.contains("\"catalogs/{}\", encode_path_value(& request.name, false)"),
"GET path templating missing:\n{client}"
);
assert!(
client.contains("fn encode_path_value("),
"path encoding helper missing"
);
assert!(
client.contains("self.base_url.join(\"catalogs\")"),
"collection URL join missing"
);
assert!(
client.contains("append_pair(\"max_results\"")
&& client.contains("append_pair(\"page_token\""),
"list query-param appends missing"
);
assert!(
client.contains("if !response.status().is_success()")
&& client.contains("parse_error_response(response)"),
"client error-handling missing"
);
assert!(client.contains(".post(url)"), "POST verb missing");
assert!(client.contains(".patch(url)"), "PATCH verb missing");
assert!(client.contains(".delete(url)"), "DELETE verb missing");
}
fn full_client_config(tmp: &Path) -> CodeGenConfig {
let mut config = rust_config(tmp);
config.output.generate_resource_clients = true;
config.bindings = Some(BindingsConfig {
aggregate_client_name: "ExampleClient".into(),
client_crate_name: "example_client".into(),
py_error_type: "PyExampleError".into(),
py_result_type: "PyExampleResult".into(),
napi_error_ext_trait: "NapiErrorExt".into(),
ts_error_base_class: "ExampleError".into(),
ts_error_code_prefix: "EX".into(),
python: Default::default(),
});
config
}
fn assert_client_files_parse(client_dir: &Path) {
for path in walk(client_dir)
.into_iter()
.filter(|p| p.extension().is_some_and(|e| e == "rs"))
{
let src = std::fs::read_to_string(&path).expect("read generated file");
syn::parse_file(&src)
.unwrap_or_else(|e| panic!("generated {} is invalid Rust: {e}", path.display()));
}
}
#[test]
fn client_resource_features_gate_services_on_model_package() {
let tmp = TempDir::new().unwrap();
let mut config = full_client_config(tmp.path());
config.rest_client.resource_features = true;
generate_code(&metadata(), &config).expect("generation succeeds");
let codegen = tmp.path().join("client");
let root = std::fs::read_to_string(codegen.join("mod.rs")).unwrap();
for (feature, module) in [
("catalog", "catalog"),
("catalog", "query"),
("schemas", "schema"),
("tags", "tag_assignments"),
] {
let gated = format!("#[cfg(feature = \"{feature}\")]\npub mod {module};");
assert!(root.contains(&gated), "missing `{gated}` in:\n{root}");
}
let aggregate = std::fs::read_to_string(codegen.join("client.rs")).unwrap();
assert!(
aggregate.contains("#[cfg(feature = \"schemas\")]\nimpl ExampleClient"),
"aggregate accessors are not gated per service:\n{aggregate}"
);
let catalog_resource = std::fs::read_to_string(codegen.join("catalog/resource.rs")).unwrap();
assert!(
catalog_resource.contains("#[cfg(feature = \"schemas\")]"),
"child navigators are not gated on the child's feature:\n{catalog_resource}"
);
assert_client_files_parse(&tmp.path().join("client"));
}
#[test]
fn client_output_is_ungated_by_default() {
let tmp = TempDir::new().unwrap();
generate_code(&metadata(), &full_client_config(tmp.path())).expect("generation succeeds");
let client_src = read_all(&tmp.path().join("client"));
assert!(
!client_src.contains("cfg(feature"),
"default client output must not carry feature gates"
);
assert!(
!walk(&tmp.path().join("client"))
.iter()
.any(|p| p.ends_with("handler.rs")),
"handler impls must be opt-in"
);
}
#[test]
fn client_handler_impls_forward_to_service_clients() {
let tmp = TempDir::new().unwrap();
let mut config = full_client_config(tmp.path());
config.rest_client.handler_impls = Some(olai_codegen::ClientHandlerImpls {
crate_path: "example_handlers".into(),
feature: "handlers".into(),
skip: vec!["tag_assignments".into()],
});
generate_code(&metadata(), &config).expect("generation succeeds");
let catalogs = tmp.path().join("client/catalog");
let module = std::fs::read_to_string(catalogs.join("mod.rs")).unwrap();
assert!(
module.contains(
"#[cfg(all(feature = \"handlers\", not(target_arch = \"wasm32\")))]\nmod handler;"
),
"handler module is not mounted behind the feature:\n{module}"
);
let handler = std::fs::read_to_string(catalogs.join("handler.rs")).unwrap();
assert!(
handler.contains("example_handlers::catalog::CatalogHandler<Cx>")
&& handler.contains("for CatalogServiceClient"),
"missing handler impl for the service client:\n{handler}"
);
assert!(
handler.contains("CatalogServiceClient::get_catalog(self, &request)")
&& handler.contains("-> example_handlers::Result<"),
"trait methods must forward to the inherent client method:\n{handler}"
);
assert!(
!tmp.path()
.join("client/tag_assignments/handler.rs")
.exists(),
"skipped services must not get a generated impl"
);
assert_client_files_parse(&tmp.path().join("client"));
}
#[test]
fn handler_trait_has_async_methods_per_rpc() {
let tmp = TempDir::new().unwrap();
let config = rust_config(tmp.path());
generate_code(&metadata(), &config).expect("generation succeeds");
let server = read_all(&tmp.path().join("server"));
assert!(
server.contains("pub trait CatalogHandler"),
"handler trait missing:\n{server}"
);
assert!(
server.contains("#[async_trait]"),
"async_trait attr missing"
);
for method in [
"async fn create_catalog",
"async fn get_catalog",
"async fn list_catalogs",
"async fn update_catalog",
"async fn delete_catalog",
] {
assert!(server.contains(method), "handler missing `{method}`");
}
assert!(
server.contains("context: Cx") && server.contains("-> Result<Catalog>"),
"handler method signature shape unexpected"
);
}
#[test]
fn required_query_params_have_no_serde_default() {
let tmp = TempDir::new().unwrap();
let config = rust_config(tmp.path());
generate_code(&metadata(), &config).expect("generation succeeds");
let common = read_all(&tmp.path().join("common"));
assert!(
common.contains("max_results: i32,") && common.contains("page_token: String,"),
"required query params should be plain fields:\n{common}"
);
assert!(
!common.contains("#[serde(default)]\n max_results")
&& !common.contains("#[serde(default)] max_results"),
"required query param `max_results` must NOT carry #[serde(default)]:\n{common}"
);
assert!(
common.contains("tags: Vec<String>"),
"repeated query param `tags` missing:\n{common}"
);
}
#[test]
fn resource_enum_and_conversions_generated() {
let tmp = TempDir::new().unwrap();
let config = rust_config(tmp.path());
generate_code(&metadata(), &config).expect("generation succeeds");
let labels = read_all(&tmp.path().join("models"));
assert!(
labels.contains("pub enum Resource")
&& labels.contains("Catalog(super::catalog::v1::Catalog)"),
"Resource enum variant missing:\n{labels}"
);
assert!(
labels.contains("pub enum ObjectLabel") && labels.contains("Catalog,"),
"ObjectLabel enum missing"
);
assert!(
labels.contains("impl From<super::catalog::v1::Catalog> for Resource"),
"From<Catalog> for Resource missing"
);
assert!(
labels.contains("impl TryFrom<Resource> for super::catalog::v1::Catalog"),
"TryFrom<Resource> missing"
);
assert!(
labels.contains("fn resource_label(&self) -> &ObjectLabel"),
"resource_label accessor missing"
);
}
#[test]
fn resource_registry_descriptors_generated() {
let tmp = TempDir::new().unwrap();
let config = rust_config(tmp.path());
generate_code(&metadata(), &config).expect("generation succeeds");
let labels = read_all(&tmp.path().join("models"));
assert!(
labels.contains("impl ::olai_store::Label for ObjectLabel"),
"Label impl missing:\n{labels}"
);
assert!(
labels.contains("pub static RESOURCE_DESCRIPTORS"),
"RESOURCE_DESCRIPTORS static missing"
);
assert!(
labels.contains("ObjectLabel::Catalog") && labels.contains("FieldRole::"),
"descriptor entry contents missing"
);
assert!(
labels.contains("path_names: &[\"name\"]"),
"Catalog path_names should be [\"name\"], not include pagination fields:\n{labels}"
);
assert!(
!labels.contains("page_token\", \"name"),
"pagination field leaked into path_names:\n{labels}"
);
}
#[test]
fn resource_enum_skipped_when_disabled() {
let tmp = TempDir::new().unwrap();
let mut config = rust_config(tmp.path());
config.generate_resource_enum = false;
config.generate_store_integration = false;
config.error_type_path = None;
generate_code(&metadata(), &config).expect("generation succeeds");
let labels_path = tmp.path().join("models").join("_gen").join("labels.rs");
assert!(
!labels_path.exists(),
"labels.rs should not be generated when generate_resource_enum is false"
);
}
#[test]
fn node_ts_bindings_generated() {
let tmp = TempDir::new().unwrap();
let common = tmp.path().join("common");
let node_ts = tmp.path().join("node_ts");
for d in [&common, &node_ts] {
std::fs::create_dir_all(d).expect("create dir");
}
let config = CodeGenConfig {
context_type_path: "crate::Context".into(),
result_type_path: "crate::Result".into(),
models_path_template: "example_common::models::{service}::v1".into(),
models_path_crate_template: "crate::models::{service}::v1".into(),
resource_store_crate_name: "olai_store".into(),
runtime: olai_codegen::Runtime::Prost,
transport_type_path: olai_codegen::DEFAULT_TRANSPORT_TYPE_PATH.into(),
dual_transport: false,
client_protocols: olai_codegen::ClientProtocols::default(),
connect_client_path: None,
rest_client: Default::default(),
output: CodeGenOutput {
common: Some(common),
models: None,
models_subdir: "_gen".into(),
server: None,
client: None,
python: None,
node: None,
node_ts: Some(node_ts.clone()),
wasm: None,
generate_resource_clients: false,
},
generate_resource_enum: false,
generate_store_integration: false,
error_type_path: None,
generate_object_conversions: false,
bindings: Some(BindingsConfig {
aggregate_client_name: "ExampleClient".into(),
client_crate_name: "example_client".into(),
py_error_type: "PyExampleError".into(),
py_result_type: "PyExampleResult".into(),
napi_error_ext_trait: "NapiErrorExt".into(),
ts_error_base_class: "ExampleError".into(),
ts_error_code_prefix: "EX".into(),
python: Default::default(),
}),
};
generate_code(&metadata(), &config).expect("generation succeeds");
let ts = std::fs::read_to_string(node_ts.join("client.ts")).expect("client.ts written");
assert!(
ts.contains("ExampleClient"),
"aggregate client name missing"
);
assert!(ts.contains("ExampleError"), "error base class missing");
}
#[test]
fn rest_client_and_python_options_generate_valid_code() {
let tmp = TempDir::new().unwrap();
let mut config = rust_config(tmp.path());
config.runtime = olai_codegen::Runtime::Buffa;
config.transport_type_path = olai_codegen::REQWEST_TRANSPORT_TYPE_PATH.into();
config.output.generate_resource_clients = true;
let python = tmp.path().join("python");
let package = tmp.path().join("pkg");
config.output.python = Some(python.clone());
config.rest_client = olai_codegen::RestClientOptions {
json_int64_as_number: true,
ignore_unknown_enum_values: true,
error_module: Some(Default::default()),
form_encoded_methods: vec![],
path_fields_in_body_methods: vec![],
json_query_packages: vec![],
user_agent: Some("example-client".into()),
..Default::default()
};
config.bindings = Some(BindingsConfig {
aggregate_client_name: "ExampleClient".into(),
client_crate_name: "example_client".into(),
py_error_type: "PyExampleError".into(),
py_result_type: "PyExampleResult".into(),
napi_error_ext_trait: "NapiErrorExt".into(),
ts_error_base_class: "ExampleError".into(),
ts_error_code_prefix: "EX".into(),
python: Some(olai_codegen::PythonOptions {
package: olai_codegen::PyPackageConfig {
dir: package.clone(),
name: "example".into(),
},
error_base_class: "ExampleError".into(),
module: "example._core".into(),
async_methods: true,
keyword_only: true,
public_modules: true,
user_agent: Some("example-python".into()),
exports: vec![
"crate::extras::PyHelper".into(),
"crate::extras::helper_fn".into(),
],
}),
});
generate_code(&metadata(), &config).expect("generation succeeds");
for dir in ["client", "python", "models"] {
for path in walk(&tmp.path().join(dir))
.into_iter()
.filter(|p| p.extension().is_some_and(|e| e == "rs"))
{
let src = std::fs::read_to_string(&path).unwrap();
syn::parse_file(&src)
.unwrap_or_else(|e| panic!("generated {} is invalid Rust: {e}", path.display()));
}
}
let client = read_all(&tmp.path().join("client"));
assert!(
client.contains("pub struct HttpClient"),
"reqwest transport missing"
);
assert!(
client.contains("pub struct ApiError"),
"error module missing"
);
assert!(client.contains("ignore_unknown_enum_values(true)"));
assert!(
client.contains("\"example-client/\"") && client.contains("not(target_arch = \"wasm32\")"),
"default User-Agent missing"
);
assert!(client.contains("pub fn with_user_agent("));
let models = read_all(&tmp.path().join("models"));
assert!(
models.contains("pub mod json_int64"),
"int64 helpers missing"
);
assert!(models.contains("frozen"), "frozen models missing");
assert!(
models.contains("const INPUT_TYPE"),
"enum type hints missing"
);
assert!(models.contains("__python_repr"), "Python repr missing");
let bindings = read_all(&python);
assert!(bindings.contains("_async"), "async twins missing");
assert!(
bindings.contains("#[pymodule_export]\n use crate::extras::PyHelper;")
&& bindings.contains("use crate::extras::helper_fn;"),
"hand-written exports missing from the extension module"
);
assert!(
bindings.contains("trait ApiErrorAnswer")
&& bindings.contains("impl ApiErrorAnswer for ClientError"),
"client error mapping missing"
);
assert!(
bindings.contains("#[pyo3(name = \"_core\", module = \"example\")]")
&& bindings.contains("pub mod extension_module"),
"extension module missing"
);
assert!(bindings.contains("PyFuture"), "PyFuture helper missing");
assert!(
bindings.contains("\"example-python/\""),
"Python User-Agent missing"
);
assert!(bindings.contains("pub fn rt()"), "runtime missing");
let enums = std::fs::read_to_string(package.join("_enums.py")).unwrap();
assert!(enums.contains("(enum.StrEnum):"), "{enums}");
assert!(
!enums.contains("_UNSPECIFIED ="),
"hidden zero values: {enums}"
);
let schemas = std::fs::read_to_string(package.join("schemas.py")).unwrap();
assert!(
schemas.contains("from example._core.schemas import (\n DeleteSchemaResponse,")
&& schemas.contains(" SchemaClient,\n)")
&& schemas.contains("from example._enums import (\n GetSchemaRequestView,")
&& schemas.contains(" \"SchemaType\",\n]"),
"{schemas}"
);
assert!(
bindings.contains("extends = ExampleError")
&& bindings.contains("use crate::codegen::error::{\n ExampleError,"),
"PyO3 exception classes missing"
);
}
#[test]
fn no_common_output_leaves_out_the_extractors() {
let tmp = TempDir::new().unwrap();
let mut config = rust_config(tmp.path());
config.output.common = None;
config.output.server = None;
generate_code(&metadata(), &config).expect("generation succeeds");
let models = tmp.path().join("models/_gen");
assert!(
!models.join("extractors").exists(),
"no extractors without `common`"
);
let models_mod = std::fs::read_to_string(models.join("mod.rs")).expect("models mod.rs");
assert!(
!models_mod.contains("extractors"),
"the models mod.rs must not declare the extractors"
);
syn::parse_file(&models_mod).expect("the models mod.rs is valid Rust");
}
#[test]
fn server_output_needs_the_common_output() {
let tmp = TempDir::new().unwrap();
let mut config = rust_config(tmp.path());
config.output.common = None;
let err = generate_code(&metadata(), &config).expect_err("a server needs `common`");
assert!(err.to_string().contains("CodeGenOutput::common"), "{err}");
}