use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{EnumDef, EnumVariant, FieldDef, TypeDef, TypeRef};
use crate::e2e::codegen::go::snippet::render_snippet_body;
use crate::e2e::config::{ArgMapping, CallOverride, E2eConfig};
use crate::e2e::fixture::Fixture;
pub(super) fn field(name: &str, ty: TypeRef, optional: bool) -> FieldDef {
FieldDef {
name: name.into(),
ty,
optional,
..FieldDef::default()
}
}
fn request_type(fields: Vec<FieldDef>) -> TypeDef {
TypeDef {
name: "SampleRequest".into(),
rust_path: "samplelib::SampleRequest".into(),
fields,
..TypeDef::default()
}
}
pub(super) fn target_type() -> TypeDef {
TypeDef {
name: "SampleTarget".into(),
rust_path: "samplelib::SampleTarget".into(),
fields: vec![field("name", TypeRef::String, false)],
..TypeDef::default()
}
}
fn tagged_enum() -> EnumDef {
EnumDef {
name: "SampleTagged".into(),
rust_path: "samplelib::SampleTagged".into(),
variants: vec![EnumVariant {
name: "Explicit".into(),
fields: vec![field("_0", TypeRef::Named("SampleTarget".into()), false)],
..EnumVariant::default()
}],
serde_rename_all: Some("snake_case".into()),
serde_tag: Some("kind".into()),
..EnumDef::default()
}
}
fn request_arg() -> ArgMapping {
ArgMapping {
name: "req".into(),
field: "input".into(),
arg_type: "json_object".into(),
optional: false,
owned: true,
element_type: Some("SampleRequest".into()),
go_type: None,
vec_inner_is_ref: false,
trait_name: None,
}
}
fn e2e(client_factory: bool) -> E2eConfig {
let mut e2e = E2eConfig::default();
e2e.call.function = "chat".into();
e2e.call.module = "example.com/sample".into();
e2e.call.result_var = "result".into();
e2e.call.returns_result = true;
e2e.call.args = vec![request_arg()];
if client_factory {
e2e.call.overrides.insert(
"go".into(),
CallOverride {
client_factory: Some("create_client".into()),
..CallOverride::default()
},
);
}
e2e
}
pub(super) fn render(
input: serde_json::Value,
fields: Vec<FieldDef>,
extra_types: Vec<TypeDef>,
enums: &[EnumDef],
client_factory: bool,
) -> String {
let fixture = Fixture {
id: "send_request".into(),
description: "Send a request".into(),
input,
..Fixture::default()
};
let mut type_defs = vec![request_type(fields)];
type_defs.extend(extra_types);
render_snippet_body(
&fixture,
&e2e(client_factory),
&ResolvedCrateConfig::default(),
&type_defs,
enums,
&[],
)
.expect("snippet renders")
}
#[test]
fn a_string_array_field_is_kept() {
let rendered = render(
serde_json::json!({"model": "m", "tags": ["a", "b"]}),
vec![
field("model", TypeRef::String, false),
field("tags", TypeRef::Vec(Box::new(TypeRef::String)), false),
],
Vec::new(),
&[],
false,
);
assert!(rendered.contains("Tags: []string{`a`, `b`},"), "{rendered}");
}
#[test]
fn a_primitive_array_field_is_kept() {
let rendered = render(
serde_json::json!({"weights": [1, 2]}),
vec![field(
"weights",
TypeRef::Vec(Box::new(TypeRef::Primitive(crate::core::ir::PrimitiveType::U32))),
false,
)],
Vec::new(),
&[],
false,
);
assert!(rendered.contains("Weights: []uint32{1, 2},"), "{rendered}");
}
#[test]
fn a_struct_array_field_is_kept_element_by_element() {
let rendered = render(
serde_json::json!({"targets": [{"name": "one"}, {"name": "two"}]}),
vec![field(
"targets",
TypeRef::Vec(Box::new(TypeRef::Named("SampleTarget".into()))),
false,
)],
vec![target_type()],
&[],
false,
);
assert!(rendered.contains("Targets: []pkg.SampleTarget{"), "{rendered}");
assert!(rendered.contains("Name: `one`"), "{rendered}");
assert!(rendered.contains("Name: `two`"), "{rendered}");
}
#[test]
fn a_tagged_union_array_field_keeps_every_element_with_its_discriminator() {
let rendered = render(
serde_json::json!({"model": "m", "items": [{"kind": "explicit", "name": "search"}]}),
vec![
field("model", TypeRef::String, false),
field(
"items",
TypeRef::Vec(Box::new(TypeRef::Named("SampleTagged".into()))),
false,
),
],
vec![target_type()],
&[tagged_enum()],
false,
);
assert!(rendered.contains("Items: []pkg.SampleTagged{"), "{rendered}");
assert!(rendered.contains("Kind: `explicit`"), "{rendered}");
assert!(rendered.contains("Name: `search`"), "{rendered}");
}
#[test]
fn an_optional_array_field_is_not_pointer_wrapped() {
let rendered = render(
serde_json::json!({"tags": ["a"]}),
vec![field("tags", TypeRef::Vec(Box::new(TypeRef::String)), true)],
Vec::new(),
&[],
false,
);
assert!(rendered.contains("Tags: []string{`a`},"), "{rendered}");
assert!(!rendered.contains("ptr("), "{rendered}");
}
#[test]
fn a_non_array_value_for_an_array_field_is_still_omitted() {
let rendered = render(
serde_json::json!({"model": "m", "tags": "not-an-array"}),
vec![
field("model", TypeRef::String, false),
field("tags", TypeRef::Vec(Box::new(TypeRef::String)), false),
],
Vec::new(),
&[],
false,
);
assert!(rendered.contains("Model: `m`"), "{rendered}");
assert!(!rendered.contains("Tags"), "{rendered}");
}
#[test]
fn an_array_with_an_unrenderable_element_is_omitted_whole() {
let rendered = render(
serde_json::json!({"model": "m", "tags": ["a", 7]}),
vec![
field("model", TypeRef::String, false),
field("tags", TypeRef::Vec(Box::new(TypeRef::String)), false),
],
Vec::new(),
&[],
false,
);
assert!(!rendered.contains("Tags"), "{rendered}");
}
pub(super) fn assert_gofmt_stable(rendered: &str) {
let Ok(mut child) = std::process::Command::new("gofmt")
.arg("-e")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
else {
return;
};
use std::io::Write as _;
child
.stdin
.take()
.expect("gofmt stdin")
.write_all(rendered.as_bytes())
.expect("write Go snippet");
let output = child.wait_with_output().expect("wait for gofmt");
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
assert_eq!(
String::from_utf8(output.stdout)
.expect("gofmt output is UTF-8")
.trim_end(),
rendered,
"gofmt rewrote the snippet"
);
}
#[test]
fn a_scalar_followed_by_a_multi_line_array_is_gofmt_stable() {
let rendered = render(
serde_json::json!({"model": "m", "messages": [{"kind": "explicit", "name": "search"}]}),
vec![
field("model", TypeRef::String, false),
field(
"messages",
TypeRef::Vec(Box::new(TypeRef::Named("SampleTagged".into()))),
false,
),
],
vec![target_type()],
&[tagged_enum()],
false,
);
assert!(rendered.contains("Model: `m`,"), "{rendered}");
assert_gofmt_stable(&rendered);
}
#[test]
fn a_client_factory_snippet_keeps_the_array_and_indents_the_client_line() {
let rendered = render(
serde_json::json!({"model": "m", "messages": [{"kind": "explicit", "name": "search"}]}),
vec![
field("model", TypeRef::String, false),
field(
"messages",
TypeRef::Vec(Box::new(TypeRef::Named("SampleTagged".into()))),
false,
),
],
vec![target_type()],
&[tagged_enum()],
true,
);
assert!(rendered.contains("Messages: []pkg.SampleTagged{"), "{rendered}");
assert!(
rendered.contains("\n\tclient, clientErr := pkg.CreateClient("),
"the client construction must sit at one tab:\n{rendered}"
);
assert!(
!rendered.contains("\t\tclient, clientErr"),
"the client construction must not be double-indented:\n{rendered}"
);
assert_gofmt_stable(&rendered);
}