use std::collections::HashMap;
use std::path::Path;
use olai_codegen::parsing::types::{BaseType, UnifiedType};
use olai_codegen::parsing::{
CodeGenMetadata, HttpPattern, MessageField, MessageInfo, MethodMetadata, ServiceInfo,
};
use olai_codegen::{ClientProtocols, CodeGenConfig, CodeGenOutput, generate_code};
use tempfile::TempDir;
fn string_field(name: &str, required: bool) -> MessageField {
MessageField {
name: name.to_string(),
unified_type: UnifiedType {
base_type: BaseType::String,
is_optional: !required,
is_repeated: false,
},
documentation: None,
oneof_variants: None,
field_behavior: if required {
vec![olai_codegen::FieldBehavior::Required]
} else {
vec![]
},
is_sensitive: false,
resource_reference: None,
}
}
fn connect_metadata() -> CodeGenMetadata {
let request = MessageInfo {
name: "GetThingRequest".to_string(),
fields: vec![string_field("name", true), string_field("filter", false)],
resource_descriptor: None,
documentation: None,
};
let response = MessageInfo {
name: "Thing".to_string(),
fields: vec![],
resource_descriptor: None,
documentation: None,
};
let mut messages = HashMap::new();
messages.insert("GetThingRequest".to_string(), request);
messages.insert("Thing".to_string(), response);
let method = MethodMetadata {
service_name: "ReadService".to_string(),
method_name: "GetThing".to_string(),
input_type: "GetThingRequest".to_string(),
output_type: "Thing".to_string(),
operation: None,
http_rule: Default::default(),
http_pattern: HttpPattern::default(),
documentation: Some("Fetch one thing.".to_string()),
};
let service = ServiceInfo {
name: "ReadService".to_string(),
package: "example.read.v1".to_string(),
documentation: None,
methods: vec![method],
};
let mut services = HashMap::new();
services.insert("ReadService".to_string(), service);
CodeGenMetadata {
messages,
services,
enums: HashMap::new(),
}
}
fn connect_config(client_dir: &Path) -> CodeGenConfig {
CodeGenConfig {
context_type_path: "crate::Context".into(),
result_type_path: "crate::Result".into(),
models_path_template: "myproto::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_codegen::DEFAULT_TRANSPORT_TYPE_PATH.into(),
client_protocols: ClientProtocols {
rest: false,
connect: true,
},
connect_client_path: Some("myproto::connect_gen::example::read::v1".into()),
output: CodeGenOutput {
common: client_dir.join("common"),
models: None,
models_subdir: "_gen".into(),
server: None,
client: Some(client_dir.to_path_buf()),
python: None,
node: None,
node_ts: None,
wasm: None,
python_typings_filename: "client.pyi".into(),
generate_resource_clients: false,
},
generate_resource_enum: false,
generate_store_integration: false,
error_type_path: None,
generate_object_conversions: false,
bindings: None,
}
}
fn read(path: &Path) -> String {
std::fs::read_to_string(path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()))
}
#[test]
fn connect_client_is_generated_without_http_annotations() {
let tmp = TempDir::new().unwrap();
let client = tmp.path().join("client");
std::fs::create_dir_all(client.join("common")).unwrap();
let metadata = connect_metadata();
let config = connect_config(&client);
generate_code(&metadata, &config).expect("connect generation succeeds");
let client_rs = read(&client.join("read/client.rs"));
let builders_rs = read(&client.join("read/builders.rs"));
assert!(
client_rs.contains("ReadServiceConnectClient"),
"missing Connect adapter type:\n{client_rs}"
);
assert!(
client_rs.contains("myproto :: connect_gen :: example :: read :: v1 :: ReadServiceClient")
|| client_rs.contains("ReadServiceClient"),
"adapter must wrap the connect-rust client:\n{client_rs}"
);
assert!(
client_rs.contains("CloudTransport"),
"adapter should default to the cloud transport:\n{client_rs}"
);
assert!(client_rs.contains("from_connect_error"), "{client_rs}");
assert!(client_rs.contains("into_owned"), "{client_rs}");
assert!(
client_rs.contains("async fn get_thing"),
"adapter method should be snake_case:\n{client_rs}"
);
assert!(builders_rs.contains("GetThingBuilder"), "{builders_rs}");
assert!(
builders_rs.contains("name") && builders_rs.contains("fn with_filter"),
"required `name` as ctor arg + optional `filter` as with_*:\n{builders_rs}"
);
assert!(
!builders_rs.contains("fn with_name"),
"required field must not get a with_* setter:\n{builders_rs}"
);
for f in [
"read/client.rs",
"read/builders.rs",
"read/mod.rs",
"mod.rs",
] {
let src = read(&client.join(f));
syn::parse_file(&src).unwrap_or_else(|e| panic!("{f} does not parse: {e}\n{src}"));
}
}
#[test]
fn both_protocols_emit_side_by_side_under_subdirs() {
let tmp = TempDir::new().unwrap();
let client = tmp.path().join("client");
std::fs::create_dir_all(client.join("common")).unwrap();
let metadata = connect_metadata();
let mut config = connect_config(&client);
config.client_protocols = ClientProtocols {
rest: true,
connect: true,
};
generate_code(&metadata, &config).expect("dual generation succeeds");
let connect_client = read(&client.join("read/connect/client.rs"));
assert!(
connect_client.contains("ReadServiceConnectClient"),
"{connect_client}"
);
for f in [
"read/connect/client.rs",
"read/connect/builders.rs",
"read/connect/mod.rs",
"read/rest/client.rs",
"read/rest/mod.rs",
"read/mod.rs",
"mod.rs",
] {
let path = client.join(f);
assert!(path.exists(), "expected generated file {f}");
let src = read(&path);
syn::parse_file(&src).unwrap_or_else(|e| panic!("{f} does not parse: {e}\n{src}"));
}
let service_mod = read(&client.join("read/mod.rs"));
assert!(service_mod.contains("pub mod rest"), "{service_mod}");
assert!(service_mod.contains("pub mod connect"), "{service_mod}");
}