use heck::{ToLowerCamelCase, ToUpperCamelCase};
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{EnumDef, TypeDef};
use crate::e2e::config::E2eConfig;
use crate::e2e::fixture::{Fixture, FixtureEnv};
use super::args::{KotlinArgsContext, build_args_and_setup};
const TEST_CLASS_MAPPER_REFERENCE: &str = "MAPPER.";
const SNIPPET_MAPPER_REFERENCE: &str = "mapper.";
fn rebind_mapper_references(source: &str) -> String {
source.replace(TEST_CLASS_MAPPER_REFERENCE, SNIPPET_MAPPER_REFERENCE)
}
pub(crate) fn render_snippet_body(
fixture: &Fixture,
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[TypeDef],
enums: &[EnumDef],
kotlin_android_style: bool,
) -> anyhow::Result<String> {
let lang = if kotlin_android_style {
"kotlin_android"
} else {
"kotlin"
};
let mut call = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
call = crate::e2e::codegen::select_best_matching_call(call, e2e_config, fixture);
let recipe = crate::e2e::codegen::recipe::ResolvedE2eCallRecipe::resolve(lang, fixture, call, type_defs);
let overrides = recipe.override_config;
let class_name = overrides
.and_then(|value| value.class.as_deref())
.unwrap_or(&config.name)
.rsplit('.')
.next()
.unwrap_or(&config.name)
.to_upper_camel_case();
let function_name = overrides
.and_then(|value| value.function.as_deref())
.unwrap_or(&call.function)
.to_lower_camel_case();
let options_type = recipe
.options_type
.or_else(|| recipe.compatible_options_type(&["kotlin", "kotlin_android", "java", "csharp"]));
let adapter_lookup_name = call.core_lookup_name(lang);
let adapter = adapter_lookup_name
.as_deref()
.and_then(|name| config.adapters.iter().find(|a| a.name == name));
let is_streaming_owner_adapter = adapter.is_some_and(|a| {
matches!(a.pattern, crate::core::config::extras::AdapterPattern::Streaming) && a.owner_type.is_some()
});
let streaming_owner_handle: Option<String> = if is_streaming_owner_adapter {
recipe
.args
.iter()
.find(|a| a.arg_type == "handle")
.map(|a| a.name.clone())
} else {
None
};
let streaming_request = adapter.and_then(|a| {
matches!(a.pattern, crate::core::config::extras::AdapterPattern::Streaming)
.then(|| a.params.first())
.flatten()
});
let call_args: std::borrow::Cow<'_, [crate::e2e::config::ArgMapping]> =
if streaming_owner_handle.is_some() && streaming_request.is_some() {
std::borrow::Cow::Owned(recipe.args.iter().filter(|a| a.arg_type == "handle").cloned().collect())
} else {
std::borrow::Cow::Borrowed(recipe.args)
};
let (setup_lines, mut args) = build_args_and_setup(
&fixture.input,
&call_args,
KotlinArgsContext {
fixture,
class_name: &class_name,
options_type,
fixture_id: &fixture.id,
kotlin_android_style,
config,
type_defs,
owner_handle_is_receiver: streaming_owner_handle.is_some(),
},
)?;
let mut setup_lines = setup_lines
.into_iter()
.map(|line| rebind_mapper_references(&line))
.collect::<Vec<_>>();
if streaming_owner_handle.is_some()
&& let Some(request) = streaming_request
{
let request_name = request.name.to_lower_camel_case();
let request_type = request.ty.rsplit("::").next().unwrap_or(&request.ty);
let mut request_input = fixture.input.clone();
if let Some(object) = request_input.as_object_mut() {
for handle in recipe.args.iter().filter(|arg| arg.arg_type == "handle") {
let field = handle.field.strip_prefix("input.").unwrap_or(&handle.field);
object.remove(field);
}
}
let normalized = crate::e2e::codegen::transform_json_keys_for_language(&request_input, "snake_case");
let request_json = serde_json::to_string(&normalized).unwrap_or_default();
let escaped_json = crate::e2e::escape::escape_kotlin(&request_json);
if crate::e2e::codegen::value_contains_mock_url_placeholder(&normalized) {
let env_key = crate::e2e::codegen::mock_url_env_key(&fixture.id);
setup_lines.push(format!(
"val {request_name}Json = \"{escaped_json}\".replace(\"{}\", System.getProperty(\"mockServer.{}\", System.getenv(\"{env_key}\") ?: \"\"))",
crate::e2e::escape::escape_kotlin(crate::e2e::codegen::MOCK_URL_PLACEHOLDER),
fixture.id,
));
setup_lines.push(format!(
"val {request_name} = mapper.readValue({request_name}Json, {request_type}::class.java)"
));
} else {
setup_lines.push(format!(
"val {request_name} = mapper.readValue(\"{escaped_json}\", {request_type}::class.java)"
));
}
args = request_name;
}
if let Some(visitor) = &fixture.visitor
&& let Some(visitor_args) =
super::visitor::attach_visitor(&mut setup_lines, &args, visitor, config, type_defs, enums)
{
args = visitor_args;
}
if !recipe.extra_args.is_empty() {
args = if args.is_empty() {
recipe.extra_args.join(", ")
} else {
format!("{args}, {}", recipe.extra_args.join(", "))
};
}
args = rebind_mapper_references(&args);
let client_factory = overrides.and_then(|value| value.client_factory.as_deref()).or_else(|| {
e2e_config
.call
.overrides
.get(lang)
.and_then(|value| value.client_factory.as_deref())
});
let needs_mapper = args.contains(SNIPPET_MAPPER_REFERENCE)
|| setup_lines.iter().any(|line| line.contains(SNIPPET_MAPPER_REFERENCE));
let is_async = client_factory.is_some() || call.r#async;
let package_name = if kotlin_android_style {
config
.kotlin_android
.as_ref()
.and_then(|value| value.package.clone())
.unwrap_or_else(|| config.kotlin_package())
} else {
config.kotlin_package()
};
let expects_error = fixture
.assertions
.iter()
.any(|assertion| assertion.assertion_type == "error");
let api_key_var = FixtureEnv::api_key_var_or_default(fixture.env.as_ref());
let base_url = crate::e2e::codegen::client_factory::docs_base_url(fixture.docs_client())
.map(crate::e2e::escape::escape_kotlin);
let call_target_class_name = if client_factory.is_none() {
streaming_owner_handle.unwrap_or(class_name)
} else {
class_name
};
let presentation = crate::e2e::codegen::presentation::resolve(fixture, e2e_config, lang);
let result_var = call.effective_result_var();
Ok(crate::e2e::template_env::render(
"kotlin/snippet_body.jinja",
minijinja::context! {
package_name => package_name,
needs_mapper => needs_mapper,
setup_lines => setup_lines,
client_factory => client_factory.map(ToLowerCamelCase::to_lower_camel_case),
class_name => call_target_class_name,
function_name => function_name,
args => args,
result_var => result_var,
returns_void => call.returns_void,
is_async => is_async,
fixture_id => fixture.id,
expects_error => expects_error,
api_key_var => api_key_var,
presentation => presentation,
base_url => base_url,
},
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::e2e::config::{CallConfig, CallOverride};
fn fixture() -> Fixture {
Fixture {
id: "quick_start".into(),
description: "Quick start".into(),
input: serde_json::Value::Null,
..Fixture::default()
}
}
const BYTES_ELEMENT: &str = r#"mapper.readValue("{\"kind\":\"bytes\"}", ExtractInput::class.java)"#;
const URI_ELEMENT: &str = r#"mapper.readValue("{\"kind\":\"uri\"}", ExtractInput::class.java)"#;
fn line_containing<'a>(body: &'a str, needle: &str) -> &'a str {
body.lines()
.find(|line| line.contains(needle))
.unwrap_or_else(|| panic!("no line contains {needle} in:\n{body}"))
.trim()
}
fn batch_call() -> CallConfig {
CallConfig {
function: "extract_batch".into(),
result_var: "result".into(),
args: vec![crate::e2e::config::ArgMapping {
name: "inputs".into(),
field: "inputs".into(),
arg_type: "json_object".into(),
optional: false,
owned: false,
element_type: Some("ExtractInput".into()),
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
}],
..CallConfig::default()
}
}
fn batch_fixture() -> Fixture {
Fixture {
id: "extract_batch_bytes_happy".into(),
description: "Extract several documents in one batch".into(),
input: serde_json::json!({"inputs": [{"kind": "bytes"}, {"kind": "uri"}]}),
..Fixture::default()
}
}
fn batch_snippet(kotlin_android_style: bool) -> String {
let config = ResolvedCrateConfig {
name: "xberg".into(),
..ResolvedCrateConfig::default()
};
render_snippet_body(
&batch_fixture(),
&E2eConfig {
call: batch_call(),
..E2eConfig::default()
},
&config,
&[],
&[],
kotlin_android_style,
)
.expect("snippet renders")
}
#[test]
fn android_batch_snippet_binds_list_elements_to_the_locally_declared_mapper() {
let body = batch_snippet(true);
assert!(
!body.contains("MAPPER"),
"the snippet must not reference the test class's private MAPPER, got:\n{body}"
);
assert!(
body.contains("import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper"),
"{body}"
);
assert!(body.contains(" val mapper = jacksonObjectMapper()"), "{body}");
assert_eq!(
line_containing(&body, "extractBatch"),
format!("val result = Xberg.extractBatch(listOf({BYTES_ELEMENT}, {URI_ELEMENT}))")
);
}
#[test]
fn jvm_batch_snippet_binds_list_elements_to_the_locally_declared_mapper() {
let body = batch_snippet(false);
assert!(
!body.contains("MAPPER"),
"the snippet must not reference the test class's private MAPPER, got:\n{body}"
);
assert!(
body.contains("import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper"),
"{body}"
);
assert!(body.contains(" val mapper = jacksonObjectMapper()"), "{body}");
assert_eq!(
line_containing(&body, "extractBatch"),
format!("val result = Xberg.extractBatch(listOf({BYTES_ELEMENT}, {URI_ELEMENT}))")
);
}
#[test]
fn documented_presentation_binds_the_result_and_reads_the_shown_fields() {
let documented: Fixture = serde_json::from_value(serde_json::json!({
"id": "present_items", "description": "Present returned items", "input": null,
"docs": {"topic": "guides", "presentation": {"operations": [
{"op": "show", "path": "summary", "display": true},
{"op": "iterate", "path": "items", "item": "item", "fields": ["label"]}
]}}
}))
.expect("fixture");
let e2e = E2eConfig {
call: CallConfig {
function: "process".into(),
result_var: "result".into(),
..CallConfig::default()
},
result_fields: ["summary".to_string(), "items".to_string()].into_iter().collect(),
..E2eConfig::default()
};
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let body = render_snippet_body(&documented, &e2e, &config, &[], &[], false).expect("snippet renders");
assert!(body.contains("val result = Sample.process()"), "{body}");
assert!(body.contains("println(result.summary())"), "{body}");
assert!(body.contains("for (item in result.items()) {"), "{body}");
assert!(body.contains("println(item.label())"), "{body}");
assert!(
!body.contains("println(result)"),
"the whole-result fallback must give way to the documented presentation:\n{body}"
);
}
#[test]
fn snippet_keeps_the_native_call_without_the_test_harness() {
let mut call = CallConfig {
function: "load_document".into(),
result_var: "document".into(),
..CallConfig::default()
};
call.overrides.insert(
"kotlin".into(),
CallOverride {
class: Some("DocumentApi".into()),
..CallOverride::default()
},
);
let body = render_snippet_body(
&fixture(),
&E2eConfig {
call,
..E2eConfig::default()
},
&ResolvedCrateConfig::default(),
&[],
&[],
false,
)
.expect("snippet renders");
assert!(body.contains("DocumentApi.loadDocument()"));
assert!(body.contains("println(document)"), "{body}");
assert!(body.contains("fun main()"));
assert!(!body.contains("@Test"));
assert!(!body.contains("assert"));
}
#[test]
fn client_factory_snippet_never_points_the_reader_at_the_mock_server() {
let fixture = Fixture {
id: "rate_limit_429".into(),
description: "Rate limited".into(),
input: serde_json::Value::Null,
..Fixture::default()
};
let mut call = CallConfig {
function: "chat".into(),
result_var: "result".into(),
..CallConfig::default()
};
call.overrides.insert(
"kotlin".into(),
CallOverride {
client_factory: Some("create_client".into()),
..CallOverride::default()
},
);
let body = render_snippet_body(
&fixture,
&E2eConfig {
call,
..E2eConfig::default()
},
&ResolvedCrateConfig::default(),
&[],
&[],
false,
)
.expect("snippet renders");
assert!(!body.contains("MOCK_SERVER"), "mock-server env var leaked:\n{body}");
assert!(!body.contains("mockServer"), "mock-server property leaked:\n{body}");
assert!(
!body.contains("/fixtures/rate_limit_429"),
"mock-server fixture route leaked:\n{body}"
);
assert!(!body.contains("\"test-key\""), "literal credential leaked:\n{body}");
assert!(
body.contains("System.getenv(\"API_KEY\")"),
"credential is not read from the environment:\n{body}"
);
assert!(
body.contains("createClient(apiKey = apiKey)"),
"an unconfigured project must construct the client without a mock base URL:\n{body}"
);
}
fn client_release_snippet(expects_error: bool) -> String {
let mut fixture = Fixture {
id: "rate_limit_429".into(),
description: "Rated limited".into(),
input: serde_json::Value::Null,
..Fixture::default()
};
if expects_error {
fixture.assertions = serde_json::from_value(serde_json::json!([{"type": "error"}])).expect("assertions");
}
let mut call = CallConfig {
function: "chat".into(),
result_var: "result".into(),
..CallConfig::default()
};
call.overrides.insert(
"kotlin".into(),
CallOverride {
client_factory: Some("create_client".into()),
..CallOverride::default()
},
);
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
render_snippet_body(
&fixture,
&E2eConfig {
call,
..E2eConfig::default()
},
&config,
&[],
&[],
false,
)
.expect("snippet renders")
}
#[test]
fn client_factory_snippet_releases_the_client_in_a_use_block() {
let body = client_release_snippet(false);
assert!(
body.contains("Sample.createClient(apiKey = apiKey).use { client -> client.chat() }"),
"the client must be released via a `use` block around the call:\n{body}"
);
assert!(
!body.contains("client.close()"),
"no bare close() call must remain:\n{body}"
);
}
#[test]
fn client_factory_snippet_releases_the_client_on_the_error_path() {
let body = client_release_snippet(true);
let try_open = body
.find(" try {")
.expect("expects-error snippet still opens a try block");
let use_block = body
.find("Sample.createClient(apiKey = apiKey).use { client -> client.chat() }")
.expect("client construction moves inside the try, unchanged in shape");
let catch_clause = body.find("catch (error: Exception)").expect("catch clause present");
assert!(
try_open < use_block && use_block < catch_clause,
"the use block must sit inside the try that the catch closes:\n{body}"
);
assert!(
!body.contains("client.close()"),
"no bare close() call must remain:\n{body}"
);
}
#[test]
fn snippet_without_a_client_factory_is_unchanged() {
let fixture: Fixture = serde_json::from_value(serde_json::json!({
"id": "invalid_input", "description": "Reject invalid input", "input": null,
"assertions": [{"type": "error"}]
}))
.expect("fixture");
let mut e2e = E2eConfig::default();
e2e.call.function = "process".into();
let config = ResolvedCrateConfig {
name: "sample".into(),
..ResolvedCrateConfig::default()
};
let body = render_snippet_body(&fixture, &e2e, &config, &[], &[], false).expect("snippet renders");
assert!(
!body.contains(".use {"),
"a snippet that constructs no client must emit no use block:\n{body}"
);
assert!(
!body.contains("close()"),
"a snippet that constructs no client must emit no close call:\n{body}"
);
assert!(
body.contains(" val result = Sample.process()"),
"the plain call must be unchanged:\n{body}"
);
}
#[test]
fn a_snippet_renders_the_base_url_the_fixture_documents() {
let fixture: Fixture = serde_json::from_value(serde_json::json!({
"id": "custom_base_url",
"description": "Custom base URL",
"input": null,
"docs": {
"topic": "configuration",
"client": {"base_url": "https://llm.internal.example.com/v1"}
}
}))
.expect("fixture");
let mut call = CallConfig {
function: "chat".into(),
result_var: "result".into(),
..CallConfig::default()
};
call.overrides.insert(
"kotlin".into(),
CallOverride {
client_factory: Some("create_client".into()),
..CallOverride::default()
},
);
let body = render_snippet_body(
&fixture,
&E2eConfig {
call,
..E2eConfig::default()
},
&ResolvedCrateConfig::default(),
&[],
&[],
false,
)
.expect("snippet renders");
assert!(
body.contains("createClient(apiKey = apiKey, baseUrl = \"https://llm.internal.example.com/v1\")"),
"the snippet for a custom-base-url topic must show the custom base URL:\n{body}"
);
}
#[test]
fn a_fixture_without_a_docs_client_keeps_the_bare_client_construction_call() {
let fixture = Fixture {
id: "rate_limit_429".into(),
description: "Rate limited".into(),
input: serde_json::Value::Null,
..Fixture::default()
};
let mut call = CallConfig {
function: "chat".into(),
result_var: "result".into(),
..CallConfig::default()
};
call.overrides.insert(
"kotlin".into(),
CallOverride {
client_factory: Some("create_client".into()),
..CallOverride::default()
},
);
let body = render_snippet_body(
&fixture,
&E2eConfig {
call,
..E2eConfig::default()
},
&ResolvedCrateConfig::default(),
&[],
&[],
false,
)
.expect("snippet renders");
assert!(
body.contains("createClient(apiKey = apiKey)"),
"an unconfigured project must construct the client without a base URL:\n{body}"
);
assert!(
!body.contains("baseUrl"),
"no docs client must mean no baseUrl argument:\n{body}"
);
}
#[test]
fn android_snippet_uses_simple_class_name_and_sync_main() {
let mut call = CallConfig {
function: "convert".into(),
result_var: "result".into(),
..CallConfig::default()
};
call.overrides.insert(
"kotlin_android".into(),
CallOverride {
class: Some("dev.sample.SampleApi".into()),
..CallOverride::default()
},
);
let mut config = ResolvedCrateConfig::default();
config.kotlin_android = Some(crate::core::config::KotlinAndroidConfig {
package: Some("dev.sample".into()),
..Default::default()
});
let body = render_snippet_body(
&fixture(),
&E2eConfig {
call,
..E2eConfig::default()
},
&config,
&[],
&[],
true,
)
.expect("snippet renders");
assert!(body.contains("import dev.sample.*"), "{body}");
assert!(body.contains("SampleApi.convert()"), "{body}");
assert!(body.contains("fun main() {"), "{body}");
assert!(!body.contains("runBlocking"), "{body}");
assert!(!body.contains("DevSampleSampleApi"), "{body}");
}
#[test]
fn android_snippet_declares_typed_config_without_coroutine_wrapper() {
let fixture: Fixture = serde_json::from_value(serde_json::json!({
"id": "process_source",
"description": "Process source",
"input": {
"source_code": "fn main() {}",
"config": {"language": "rust"}
}
}))
.expect("fixture parses");
let mut call = CallConfig {
function: "process".into(),
result_var: "result".into(),
args: vec![
crate::e2e::config::ArgMapping {
name: "source".into(),
field: "source_code".into(),
arg_type: "string".into(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
},
crate::e2e::config::ArgMapping {
name: "config".into(),
field: "config".into(),
arg_type: "json_object".into(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
},
],
..CallConfig::default()
};
call.overrides.insert(
"java".into(),
CallOverride {
options_type: Some("ProcessConfig".into()),
..Default::default()
},
);
let config = ResolvedCrateConfig {
name: "sample_api".into(),
..ResolvedCrateConfig::default()
};
let body = render_snippet_body(
&fixture,
&E2eConfig {
call,
..E2eConfig::default()
},
&config,
&[],
&[],
true,
)
.expect("snippet renders");
assert!(body.contains("val config = mapper.readValue"), "{body}");
assert!(body.contains("ProcessConfig::class.java"), "{body}");
assert!(body.contains("SampleApi.process(\"fn main() {}\", config)"), "{body}");
assert!(!body.contains("runBlocking"), "{body}");
}
#[test]
fn snippet_renders_expected_error_as_an_executable_example() {
let fixture: Fixture = serde_json::from_value(serde_json::json!({
"id": "invalid_input", "description": "Reject invalid input", "input": null,
"assertions": [{"type": "error"}]
}))
.expect("fixture");
let mut e2e = E2eConfig::default();
e2e.call.function = "process".into();
let body = render_snippet_body(&fixture, &e2e, &ResolvedCrateConfig::default(), &[], &[], false)
.expect("snippet renders");
assert!(body.contains("catch (error: Exception)"), "{body}");
assert!(body.contains("error::class.simpleName"), "{body}");
assert!(!body.contains("AssertionError"), "{body}");
}
#[test]
fn snippet_reads_nested_typed_dto_files() {
let fixture: Fixture = serde_json::from_value(serde_json::json!({
"id": "document_input",
"description": "Read a document",
"input": {"request": {"content": "ignored"}},
"assertions": [],
"docs": {
"topic": "documents",
"presentation": {"files": [{"field": "/request/content", "path": "document.pdf"}]}
}
}))
.expect("fixture");
let mut call = CallConfig {
function: "process".into(),
args: vec![crate::e2e::config::ArgMapping {
name: "request".into(),
field: "request".into(),
arg_type: "json_object".into(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
}],
..CallConfig::default()
};
call.overrides.insert(
"kotlin".into(),
CallOverride {
options_type: Some("DocumentRequest".into()),
..CallOverride::default()
},
);
let body = render_snippet_body(
&fixture.docs_call_fixture(),
&E2eConfig {
call,
..E2eConfig::default()
},
&ResolvedCrateConfig::default(),
&[],
&[],
false,
)
.expect("snippet renders");
assert!(
body.contains("Files.readAllBytes(java.nio.file.Path.of(\"document.pdf\"))"),
"{body}"
);
assert!(body.contains("Base64.getEncoder().encodeToString"), "{body}");
assert!(body.contains("DocumentRequest::class.java"), "{body}");
}
#[test]
fn snippet_deserializes_generic_typed_dto_without_file_metadata() {
let fixture = Fixture {
id: "document_input".into(),
description: "Process a document".into(),
input: serde_json::json!({"kind": "uri", "uri": "document.txt"}),
..Fixture::default()
};
let mut call = CallConfig {
function: "process".into(),
args: vec![crate::e2e::config::ArgMapping {
name: "input".into(),
field: "input".into(),
arg_type: "json_object".into(),
optional: false,
owned: false,
element_type: Some("DocumentInput".into()),
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
}],
..CallConfig::default()
};
call.overrides.insert(
"kotlin_android".into(),
CallOverride {
options_type: Some("ExtractionConfig".into()),
..CallOverride::default()
},
);
let body = render_snippet_body(
&fixture,
&E2eConfig {
call,
..E2eConfig::default()
},
&ResolvedCrateConfig::default(),
&[],
&[],
true,
)
.expect("snippet renders");
assert!(body.contains("val input = mapper.readValue("), "{body}");
assert!(body.contains("DocumentInput::class.java"), "{body}");
assert!(!body.contains("ExtractionConfig::class.java"), "{body}");
assert!(body.contains(".process(input)"), "{body}");
assert!(body.contains("jacksonObjectMapper"), "{body}");
}
#[test]
fn snippet_uses_nested_centralized_wire_names() {
let fixture = Fixture {
id: "document_input".into(),
description: "Process a document".into(),
input: serde_json::json!({"request_id": "one", "details": {"page_count": 2}}),
..Fixture::default()
};
let mut call = CallConfig {
function: "process".into(),
args: vec![crate::e2e::config::ArgMapping {
name: "input".into(),
field: "input".into(),
arg_type: "json_object".into(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
}],
..CallConfig::default()
};
call.overrides.insert(
"kotlin_android".into(),
CallOverride {
options_type: Some("DocumentInput".into()),
..CallOverride::default()
},
);
let type_defs = vec![
crate::core::ir::TypeDef {
name: "DocumentInput".into(),
fields: vec![
crate::core::ir::FieldDef {
name: "request_id".into(),
serde_rename: Some("request-id".into()),
..Default::default()
},
crate::core::ir::FieldDef {
name: "details".into(),
ty: crate::core::ir::TypeRef::Named("DocumentDetails".into()),
..Default::default()
},
],
..Default::default()
},
crate::core::ir::TypeDef {
name: "DocumentDetails".into(),
serde_rename_all: Some("camelCase".into()),
fields: vec![crate::core::ir::FieldDef {
name: "page_count".into(),
..Default::default()
}],
..Default::default()
},
];
let body = render_snippet_body(
&fixture,
&E2eConfig {
call,
..E2eConfig::default()
},
&ResolvedCrateConfig::default(),
&type_defs,
&[],
true,
)
.expect("snippet renders");
assert!(body.contains(r#"\"request-id\":\"one\""#), "{body}");
assert!(body.contains(r#"\"pageCount\":2"#), "{body}");
assert!(body.contains("val mapper = jacksonObjectMapper()"), "{body}");
}
fn streaming_owner_adapter(function_name: &str, owner_type: &str) -> crate::core::config::extras::AdapterConfig {
crate::core::config::extras::AdapterConfig {
name: function_name.to_string(),
pattern: crate::core::config::extras::AdapterPattern::Streaming,
core_path: format!("test_core::{function_name}"),
params: vec![crate::core::config::extras::AdapterParam {
name: "request".to_string(),
ty: "sample::StreamRequest".to_string(),
optional: false,
}],
returns: None,
error_type: None,
owner_type: Some(owner_type.to_string()),
item_type: Some("Item".to_string()),
gil_release: false,
trait_name: None,
trait_method: None,
detect_async: false,
request_type: None,
skip_languages: Vec::new(),
}
}
fn streaming_owner_call() -> CallConfig {
CallConfig {
function: "stream_items".into(),
result_var: "result".into(),
args: vec![
crate::e2e::config::ArgMapping {
name: "handle".into(),
field: "input.handle".into(),
arg_type: "handle".into(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
},
crate::e2e::config::ArgMapping {
name: "url".into(),
field: "input.url".into(),
arg_type: "string".into(),
optional: false,
owned: false,
element_type: None,
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
},
],
..CallConfig::default()
}
}
fn streaming_owner_fixture() -> Fixture {
Fixture {
id: "stream_basic".into(),
description: "Stream items".into(),
input: serde_json::json!({ "handle": {}, "url": "https://example.com" }),
..Fixture::default()
}
}
#[test]
fn kotlin_snippet_binds_the_declared_request_before_the_call() {
let config = ResolvedCrateConfig {
name: "sample".into(),
adapters: vec![streaming_owner_adapter("stream_items", "Engine")],
..ResolvedCrateConfig::default()
};
let body = render_snippet_body(
&streaming_owner_fixture(),
&E2eConfig {
call: streaming_owner_call(),
..E2eConfig::default()
},
&config,
&[],
&[],
false,
)
.expect("snippet renders");
assert_eq!(
line_containing(&body, "val request ="),
r#"val request = mapper.readValue("{\"url\":\"https://example.com\"}", StreamRequest::class.java)"#
);
assert_eq!(
line_containing(&body, "val result ="),
"val result = handle.streamItems(request)"
);
assert!(
!body.contains("\\\"handle\\\""),
"owner handle config must not leak into the request JSON:\n{body}"
);
assert!(
!body.contains("streamItems(\"https://example.com\")"),
"the raw url must not be passed positionally in place of the declared request:\n{body}"
);
}
#[test]
fn kotlin_android_snippet_binds_the_declared_request_before_the_call() {
let config = ResolvedCrateConfig {
name: "sample".into(),
adapters: vec![streaming_owner_adapter("stream_items", "Engine")],
..ResolvedCrateConfig::default()
};
let body = render_snippet_body(
&streaming_owner_fixture(),
&E2eConfig {
call: streaming_owner_call(),
..E2eConfig::default()
},
&config,
&[],
&[],
true,
)
.expect("snippet renders");
assert_eq!(
line_containing(&body, "val request ="),
r#"val request = mapper.readValue("{\"url\":\"https://example.com\"}", StreamRequest::class.java)"#
);
assert_eq!(
line_containing(&body, "val result ="),
"val result = handle.streamItems(request)"
);
assert!(
!body.contains("\\\"handle\\\""),
"owner handle config must not leak into the request JSON:\n{body}"
);
}
}