use crate::core::config::ResolvedCrateConfig;
use crate::e2e::config::E2eConfig;
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::Fixture;
use std::collections::HashMap;
#[allow(clippy::too_many_arguments)]
pub(super) fn render_spec_file(
category: &str,
fixtures: &[&Fixture],
module_path: &str,
class_name: Option<&str>,
gem_name: &str,
options_type: Option<&str>,
enum_fields: &HashMap<String, String>,
result_is_simple: bool,
e2e_config: &E2eConfig,
needs_spec_helper: bool,
uses_harness: bool,
adapters: &[crate::core::config::extras::AdapterConfig],
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
) -> String {
let client_factory = e2e_config
.call
.overrides
.get("ruby")
.and_then(|o| o.client_factory.as_deref());
let has_http = fixtures.iter().any(|f| f.is_http_test());
let mut requires = Vec::new();
if needs_spec_helper || has_http {
requires.push("spec_helper".to_string());
}
let require_name = if module_path.is_empty() { gem_name } else { module_path };
requires.push(require_name.replace('-', "_"));
requires.push("json".to_string());
let ruby_module = super::values::ruby_module_name(module_path);
let call_receiver = class_name.map(|s| s.to_string()).unwrap_or_else(|| ruby_module.clone());
let has_array_contains = fixtures.iter().any(|fixture| {
let cc = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let fr = FieldResolver::new(
e2e_config.effective_fields(cc),
e2e_config.effective_fields_optional(cc),
e2e_config.effective_result_fields(cc),
e2e_config.effective_fields_array(cc),
&std::collections::HashSet::new(),
);
fixture.assertions.iter().any(|a| {
matches!(a.assertion_type.as_str(), "contains" | "contains_all" | "not_contains")
&& a.field
.as_deref()
.is_some_and(|f| !f.is_empty() && fr.is_array(fr.resolve(f)))
})
});
let mut examples = Vec::new();
for fixture in fixtures {
if fixture.http.is_some() {
let mut out = String::new();
if uses_harness {
super::http::render_http_example_sut(&mut out, fixture);
} else {
super::http::render_http_example(&mut out, fixture);
}
examples.push(out);
} else {
let fixture_call = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let fixture_call_resolver = FieldResolver::new(
e2e_config.effective_fields(fixture_call),
e2e_config.effective_fields_optional(fixture_call),
e2e_config.effective_result_fields(fixture_call),
e2e_config.effective_fields_array(fixture_call),
&std::collections::HashSet::new(),
);
let field_resolver = &fixture_call_resolver;
let fixture_call_overrides = fixture_call.overrides.get("ruby");
let raw_function_name = fixture_call_overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| fixture_call.function.clone());
let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
let has_usable = has_usable_assertion(fixture, field_resolver, result_is_simple);
let is_streaming = crate::e2e::codegen::streaming_assertions::resolve_is_streaming(
fixture,
fixture_call.streaming_enabled(),
);
if !expects_error && !has_usable && !has_not_error && !is_streaming && fixture.assertions.is_empty() {
let test_name = crate::e2e::escape::sanitize_ident(&fixture.id);
let description_literal =
crate::e2e::escape::ruby_string_literal(&format!("{test_name}: {}", fixture.description));
let mut out = String::new();
out.push_str(&format!(" it {description_literal} do\n"));
out.push_str(" skip 'Fixture has no assertions to validate'\n");
out.push_str(" end\n");
examples.push(out);
} else {
let fixture_function_name = if is_streaming {
raw_function_name
} else if fixture_call.r#async && !raw_function_name.ends_with("_async") {
format!("{raw_function_name}_async")
} else {
raw_function_name
};
let fixture_result_var = &fixture_call.result_var;
let fixture_args = fixture.resolved_args(fixture_call);
let fixture_client_factory = fixture_call_overrides
.and_then(|o| o.client_factory.as_deref())
.or(client_factory);
let fixture_options_type = fixture_call_overrides
.and_then(|o| o.options_type.as_deref())
.or(options_type);
let fixture_extra_args: Vec<String> =
fixture_call_overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
let fixture_result_is_simple =
fixture_call.result_is_simple || fixture_call_overrides.is_some_and(|o| o.result_is_simple);
let fixture_enum_fields: &HashMap<String, String> =
fixture_call_overrides.map(|o| &o.enum_fields).unwrap_or(enum_fields);
let adapter_req_type_owned: Option<String> = adapters
.iter()
.find(|a| a.name == fixture_call.function.as_str())
.and_then(|a| a.request_type.as_deref())
.map(|rt| rt.rsplit("::").next().unwrap_or(rt).to_string());
let streaming_item_type_owned = crate::e2e::codegen::recipe::streaming_item_type(
fixture_call,
adapters,
&[fixture_call.function.as_str()],
)
.map(str::to_string);
let example = if is_streaming {
super::examples::render_chat_stream_example(
fixture,
&fixture_function_name,
&call_receiver,
&ruby_module,
fixture_args,
fixture_options_type,
fixture_enum_fields,
e2e_config,
fixture_client_factory,
&fixture_extra_args,
adapter_req_type_owned.as_deref(),
streaming_item_type_owned.as_deref(),
config,
type_defs,
)
} else {
super::examples::render_example(
fixture,
&fixture_function_name,
&call_receiver,
&ruby_module,
fixture_result_var,
fixture_args,
field_resolver,
fixture_options_type,
fixture_enum_fields,
e2e_config.effective_fields_enum(fixture_call),
fixture_result_is_simple,
fixture_call.returns_void,
e2e_config,
fixture_client_factory,
&fixture_extra_args,
adapter_req_type_owned.as_deref(),
config,
type_defs,
)
};
examples.push(example);
}
}
}
let header = crate::core::hash::header(crate::core::hash::CommentStyle::Hash);
crate::e2e::template_env::render(
"ruby/test_file.jinja",
minijinja::context! {
category => category,
requires => requires,
has_array_contains => has_array_contains,
has_http => has_http,
examples => examples,
header => header,
},
)
}
pub(super) fn has_usable_assertion(fixture: &Fixture, field_resolver: &FieldResolver, result_is_simple: bool) -> bool {
fixture.assertions.iter().any(|a| {
if a.assertion_type == "not_error" || a.assertion_type == "error" {
return false;
}
if let Some(f) = &a.field {
if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
return false;
}
if result_is_simple {
let f_lower = f.to_lowercase();
if !f.is_empty()
&& f_lower != "content"
&& (f_lower.starts_with("metadata")
|| f_lower.starts_with("document")
|| f_lower.starts_with("structure"))
{
return false;
}
}
}
true
})
}