use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use heck::{ToKebabCase, ToSnakeCase, ToUpperCamelCase};
use indexmap::IndexMap;
use crate::SupportFiles;
use crate::error::{Error, Result};
use crate::generator::render_request_plan;
use crate::generator::{
ExternalModelBackend, GeneratedFileMap, GeneratedFileOrigin, GeneratedFiles, GenerationMode,
};
use crate::language::Language;
use crate::planning::{
PlannedFamily, PlannedJsonType, PlannedProtoType, PlannedProtoTypeInfo, PlannedResource,
PlannedResourceMethodBindingSpec as PlannedResourceMethodBinding,
PlannedResourceMethodResultKind as PlannedResourceMethodResult, PlannedSpec, PlannedType,
};
use crate::spec::{
AliasTypeSpec, EnumSpec, ExternalTypeSpec, FlagsSpec,
FunctionArgsSpec as GenericFunctionArgsSpec, FunctionFieldSpec as GenericFunctionFieldSpec,
FunctionResultSpec as GenericFunctionResultSpec, IntSpec, LanguageStringSpec, ModulePath,
OperationSpec, RecordFieldSpec, RecordSpec, SupportFragmentSpec, TypeReplacementSpec, TypeSpec,
VariantSpec,
};
use crate::spec::{ApiSpecBranch, ApiSpecLeaf, ApiSpecNode, ApiSpecTree};
use super::json_schema::go as json;
use super::proto::go as proto;
type FunctionArgsSpec = GenericFunctionArgsSpec<PlannedFamily>;
type FunctionFieldSpec = GenericFunctionFieldSpec<PlannedFamily>;
type FunctionResultSpec = GenericFunctionResultSpec<PlannedFamily>;
type PlannedOperation = OperationSpec<PlannedFamily>;
pub(in crate::generator) type PlannedTypeInfo = PlannedProtoTypeInfo;
pub(in crate::generator) const GENERATED_HEADER: &str = concat!(
"// Code generated by nexgen v",
env!("CARGO_PKG_VERSION"),
". DO NOT EDIT."
);
const TUPLE_FIELD_NAMES: &[&str] = &[
"First", "Second", "Third", "Fourth", "Fifth", "Sixth", "Seventh", "Eighth", "Ninth", "Tenth",
];
const GO_DOC_COMMENT_LINE_LENGTH: usize = 88;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct GoOptions {
pub(crate) output_dir_name: String,
}
#[derive(Debug, Clone)]
pub(in crate::generator) enum PlannedOperationOutput {
Message(PlannedMessageType),
Resource { type_name: String },
None,
}
pub(in crate::generator) type PlannedOperationResourceReturn =
crate::planning::PlannedOperationResourceReturn;
#[derive(Debug, Clone)]
struct PlannedEnum {
info: PlannedTypeInfo,
name: String,
values: Vec<PlannedEnumValue>,
}
#[derive(Debug, Clone)]
struct PlannedEnumValue {
name: String,
number: i32,
}
#[derive(Debug, Clone)]
struct PlannedFlags {
info: PlannedTypeInfo,
name: String,
flags: Vec<PlannedFlag>,
}
#[derive(Debug, Clone)]
struct PlannedFlag {
name: String,
bit: usize,
}
#[derive(Debug, Clone)]
struct PlannedVariant {
info: PlannedTypeInfo,
name: String,
type_parameters: Vec<crate::spec::TypeParameterUsage>,
cases: Vec<PlannedVariantCase>,
}
#[derive(Debug, Clone)]
struct PlannedVariantCase {
name: String,
payload: Option<PlannedValueType>,
}
#[derive(Debug, Clone)]
pub(in crate::generator) struct PlannedField {
pub(in crate::generator) proto_name: String,
pub(in crate::generator) authored_name: String,
pub(in crate::generator) doc: Option<LanguageStringSpec>,
annotation_override: Option<LanguageStringSpec>,
pub(in crate::generator) flattened_annotation_override: Option<LanguageStringSpec>,
pub(in crate::generator) required: bool,
role: PlannedFieldRole,
pub(in crate::generator) kind: PlannedFieldKind,
}
#[derive(Debug, Clone)]
enum PlannedFieldRole {
Plain,
Function(FunctionFieldSpec),
FunctionArgs(FunctionFieldSpec),
}
#[derive(Debug, Clone)]
pub(in crate::generator) enum PlannedFieldKind {
Singular(PlannedValueType),
Repeated(PlannedValueType),
Map {
key: PlannedValueType,
value: PlannedValueType,
},
}
#[derive(Debug, Clone)]
pub(in crate::generator) enum PlannedValueType {
TypeParameter(String),
Scalar(PlannedScalarType),
Enum(PlannedEnumType),
Flags(PlannedFlagsType),
Variant(PlannedVariantType),
Message(PlannedMessageType),
Tuple(Vec<PlannedValueType>),
Result {
ok: Option<Box<PlannedValueType>>,
err: Option<Box<PlannedValueType>>,
},
External {
type_name: LanguageStringSpec,
fallback: Box<PlannedValueType>,
},
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::generator) enum PlannedScalarType {
Float,
Int32,
Int64,
Bool,
String,
Bytes,
}
#[derive(Debug, Clone)]
pub(in crate::generator) struct PlannedEnumType {
pub(in crate::generator) info: Option<PlannedTypeInfo>,
pub(in crate::generator) name: Option<String>,
pub(in crate::generator) replacement: Option<TypeReplacementSpec>,
}
#[derive(Debug, Clone)]
pub(in crate::generator) struct PlannedFlagsType {
info: PlannedTypeInfo,
name: String,
}
#[derive(Debug, Clone)]
pub(in crate::generator) struct PlannedVariantType {
info: PlannedTypeInfo,
name: String,
}
#[derive(Debug, Clone)]
pub(in crate::generator) struct PlannedMessageType {
pub(in crate::generator) info: PlannedTypeInfo,
pub(in crate::generator) model_name: String,
pub(in crate::generator) replacement: Option<TypeReplacementSpec>,
pub(in crate::generator) authored_type: Option<PlannedType>,
pub(in crate::generator) source: PlannedMessageSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::generator) enum PlannedMessageSource {
Json,
Proto,
Wit,
}
fn local_type_info(full_name: &str) -> PlannedTypeInfo {
PlannedTypeInfo {
full_name: full_name.to_string(),
package: String::new(),
file_name: None,
file_options: None,
reference: LanguageStringSpec::default(),
type_name: LanguageStringSpec::default(),
}
}
fn message_model_name(full_name: &str) -> String {
full_name
.rsplit('.')
.next()
.expect("descriptor names should not be empty")
.to_upper_camel_case()
.to_string()
}
pub(in crate::generator) fn operation_output(
operation: &PlannedOperation,
spec: &PlannedSpec,
) -> PlannedOperationOutput {
match operation.output.as_ref() {
Some(TypeSpec::Resource(resource)) => resource
.wire_type
.as_ref()
.and_then(|wire_type| {
planned_message_type(&TypeSpec::External(wire_type.clone()), spec)
})
.map(PlannedOperationOutput::Message)
.unwrap_or_else(|| PlannedOperationOutput::Resource {
type_name: resource.type_name.clone(),
}),
Some(output) => planned_message_type(output, spec)
.map(PlannedOperationOutput::Message)
.unwrap_or(PlannedOperationOutput::None),
None => PlannedOperationOutput::None,
}
}
pub(in crate::generator) fn planned_message_type(
model_type: &PlannedType,
spec: &PlannedSpec,
) -> Option<PlannedMessageType> {
match model_type.without_option() {
TypeSpec::Record(record_type) => spec
.record(&record_type.full_name)
.map(planned_message_type_for_record),
TypeSpec::External(ExternalTypeSpec::Proto(PlannedProtoType::Message(message))) => {
Some(PlannedMessageType {
info: message.proto.clone(),
model_name: message.model_name.clone(),
replacement: message.replacement.clone(),
authored_type: message.authored_type.as_deref().cloned(),
source: PlannedMessageSource::Proto,
})
}
TypeSpec::External(ExternalTypeSpec::Json(json_type)) => Some(PlannedMessageType {
info: local_type_info(&json_type.full_name),
model_name: json_type.model_name.clone(),
replacement: None,
authored_type: None,
source: PlannedMessageSource::Json,
}),
TypeSpec::External(ExternalTypeSpec::Alias(AliasTypeSpec {
target, type_name, ..
})) => {
let mut info = local_type_info("alias");
info.type_name = type_name.clone();
Some(PlannedMessageType {
info,
model_name: go_authored_type_annotation(target),
replacement: None,
authored_type: Some((**target).clone()),
source: PlannedMessageSource::Wit,
})
}
_ => None,
}
}
fn planned_message_type_for_record(record: &RecordSpec<PlannedFamily>) -> PlannedMessageType {
if let Some(proto) = &record.data.proto {
PlannedMessageType {
info: proto.clone(),
model_name: record.name.clone(),
replacement: None,
authored_type: None,
source: PlannedMessageSource::Proto,
}
} else {
PlannedMessageType {
info: local_type_info(&record.full_name),
model_name: record.name.clone(),
replacement: None,
authored_type: None,
source: PlannedMessageSource::Wit,
}
}
}
fn record_message_key(record: &RecordSpec<PlannedFamily>) -> &str {
record
.data
.proto
.as_ref()
.map(|proto| proto.full_name.as_str())
.unwrap_or(record.full_name.as_str())
}
fn record_for_message<'a>(
api_plan: &'a PlannedSpec,
message: &PlannedMessageType,
) -> Option<&'a RecordSpec<PlannedFamily>> {
record_for_model_key(api_plan, &message.info.full_name)
}
pub(in crate::generator) fn record_for_model_key<'a>(
api_plan: &'a PlannedSpec,
full_name: &str,
) -> Option<&'a RecordSpec<PlannedFamily>> {
api_plan
.records()
.map(|(_, record)| record)
.find(|record| record_message_key(record) == full_name)
}
pub(in crate::generator) fn planned_field(
record: &RecordSpec<PlannedFamily>,
field_name: &str,
field: &RecordFieldSpec<PlannedFamily>,
spec: &PlannedSpec,
) -> PlannedField {
let role = if let Some(function) = &field.function {
PlannedFieldRole::Function(function.clone())
} else if let Some(function) = record.function_for_args_field(field_name) {
PlannedFieldRole::FunctionArgs(function.clone())
} else {
PlannedFieldRole::Plain
};
PlannedField {
proto_name: field_name.to_string(),
authored_name: field.name.clone(),
doc: field.doc.clone(),
annotation_override: field.annotation.clone(),
flattened_annotation_override: field.flattened_annotation.clone(),
required: field.required,
role,
kind: planned_field_kind(&field.field_type, spec),
}
}
fn planned_enum(enumeration: &EnumSpec<PlannedFamily>) -> PlannedEnum {
PlannedEnum {
info: local_type_info(&enumeration.full_name),
name: enumeration.name.clone(),
values: enumeration
.values
.iter()
.map(|value| PlannedEnumValue {
name: value.name.clone(),
number: value.number,
})
.collect(),
}
}
fn planned_flags(flags: &FlagsSpec<PlannedFamily>) -> PlannedFlags {
PlannedFlags {
info: local_type_info(&flags.full_name),
name: flags.name.clone(),
flags: flags
.flags
.iter()
.map(|flag| PlannedFlag {
name: flag.name.clone(),
bit: flag.bit,
})
.collect(),
}
}
fn planned_variant(variant: &VariantSpec<PlannedFamily>, spec: &PlannedSpec) -> PlannedVariant {
PlannedVariant {
info: local_type_info(&variant.full_name),
name: variant.name.clone(),
type_parameters: spec.variant_type_parameters(&variant.full_name, Language::Go),
cases: variant
.cases
.iter()
.map(|case| PlannedVariantCase {
name: case.name.clone(),
payload: case
.payload
.as_ref()
.map(|payload| planned_value_type(payload, spec)),
})
.collect(),
}
}
pub(in crate::generator) fn planned_field_kind(
field_type: &PlannedType,
spec: &PlannedSpec,
) -> PlannedFieldKind {
match field_type.without_option() {
TypeSpec::List(inner) => {
PlannedFieldKind::Repeated(planned_value_type(inner.without_option(), spec))
}
TypeSpec::Map(key, value) => PlannedFieldKind::Map {
key: planned_value_type(key.without_option(), spec),
value: planned_value_type(value.without_option(), spec),
},
value => PlannedFieldKind::Singular(planned_value_type(value, spec)),
}
}
fn planned_value_type(value_type: &PlannedType, spec: &PlannedSpec) -> PlannedValueType {
match value_type.without_option() {
TypeSpec::Bool => PlannedValueType::Scalar(PlannedScalarType::Bool),
TypeSpec::Int(IntSpec::I32) => PlannedValueType::Scalar(PlannedScalarType::Int32),
TypeSpec::Int(IntSpec::I64) => PlannedValueType::Scalar(PlannedScalarType::Int64),
TypeSpec::Float => PlannedValueType::Scalar(PlannedScalarType::Float),
TypeSpec::String => PlannedValueType::Scalar(PlannedScalarType::String),
TypeSpec::Bytes => PlannedValueType::Scalar(PlannedScalarType::Bytes),
TypeSpec::TypeParameter(parameter) => {
PlannedValueType::TypeParameter(parameter.name.clone())
}
TypeSpec::Record(_) => planned_message_type(value_type, spec)
.map(PlannedValueType::Message)
.unwrap_or(PlannedValueType::Unknown),
TypeSpec::Enum(enum_type) => PlannedValueType::Enum(PlannedEnumType {
info: Some(local_type_info(&enum_type.full_name)),
name: Some(enum_type.name.clone()),
replacement: None,
}),
TypeSpec::Flags(flags_type) => PlannedValueType::Flags(PlannedFlagsType {
info: local_type_info(&flags_type.full_name),
name: flags_type.name.clone(),
}),
TypeSpec::Variant(variant_type) => PlannedValueType::Variant(PlannedVariantType {
info: local_type_info(&variant_type.full_name),
name: variant_type.name.clone(),
}),
TypeSpec::Resource(_) => PlannedValueType::Unknown,
TypeSpec::External(ExternalTypeSpec::Proto(PlannedProtoType::Message(message))) => {
PlannedValueType::Message(PlannedMessageType {
info: message.proto.clone(),
model_name: message.model_name.clone(),
replacement: message.replacement.clone(),
authored_type: message.authored_type.as_deref().cloned(),
source: PlannedMessageSource::Proto,
})
}
TypeSpec::External(ExternalTypeSpec::Proto(PlannedProtoType::Enum(enum_type))) => {
PlannedValueType::Enum(PlannedEnumType {
info: Some(enum_type.proto.clone()),
name: Some(enum_type.name.clone()),
replacement: enum_type.replacement.clone(),
})
}
TypeSpec::External(ExternalTypeSpec::Json(json_type)) => {
PlannedValueType::Message(PlannedMessageType {
info: local_type_info(&json_type.full_name),
model_name: json_type.model_name.clone(),
replacement: None,
authored_type: None,
source: PlannedMessageSource::Json,
})
}
TypeSpec::External(ExternalTypeSpec::Alias(AliasTypeSpec {
target, type_name, ..
})) => PlannedValueType::External {
type_name: type_name.clone(),
fallback: Box::new(planned_value_type(target.without_option(), spec)),
},
TypeSpec::Tuple(items) => PlannedValueType::Tuple(
items
.iter()
.map(|item| planned_value_type(item, spec))
.collect(),
),
TypeSpec::Result { ok, err } => PlannedValueType::Result {
ok: ok.as_ref().map(|ok| Box::new(planned_value_type(ok, spec))),
err: err
.as_ref()
.map(|err| Box::new(planned_value_type(err, spec))),
},
TypeSpec::Option(inner) | TypeSpec::List(inner) => planned_value_type(inner, spec),
TypeSpec::Map(_, _) => PlannedValueType::Unknown,
}
}
#[derive(Debug, Clone)]
pub(in crate::generator) struct GoPackageContext {
package_name: String,
import_path: Option<String>,
}
impl GoPackageContext {
fn new(api_plan: &PlannedSpec, options: &GoOptions) -> Result<Self> {
let import_path = api_plan
.services
.first()
.and_then(|service| service.namespace.for_language(Language::Go))
.map(str::to_string);
let package_name = Self::derive_package_name(&options.output_dir_name)?;
if let Some(import_path) = &import_path {
let namespace_package_name = go_package_name(
import_path
.rsplit('/')
.next()
.filter(|segment| !segment.is_empty())
.unwrap_or(import_path),
);
if namespace_package_name != package_name {
return Err(Error::GoNamespacePackageMismatch {
namespace: import_path.clone(),
expected_package: namespace_package_name,
actual_package: package_name,
});
}
}
Ok(Self {
package_name,
import_path,
})
}
fn derive_package_name(output_dir_name: &str) -> Result<String> {
let package_name = go_package_name(output_dir_name);
if package_name.is_empty() {
return Err(Error::GoPackageNameEmpty {
output_dir_name: output_dir_name.to_string(),
});
}
Ok(package_name)
}
fn is_self_import(&self, import_path: &str) -> bool {
self.import_path
.as_deref()
.is_some_and(|self_import| self_import == import_path)
}
pub(in crate::generator) fn go_type_expr(&self, type_expr: &str) -> String {
let (import_path, code_expr) = parse_go_import(type_expr);
if import_path
.as_deref()
.is_some_and(|import_path| self.is_self_import(import_path))
{
return self.unqualify_self_expr(&code_expr);
}
code_expr
}
pub(in crate::generator) fn workflow_context_type(&self) -> String {
self.qualified_expr("go.temporal.io/sdk/workflow", "workflow.Context")
}
fn workflow_future_type(&self) -> String {
self.qualified_expr("go.temporal.io/sdk/workflow", "workflow.Future")
}
pub(in crate::generator) fn new_future(&self) -> String {
self.qualified_expr("go.temporal.io/sdk/workflow", "workflow.NewFuture")
}
fn workflow_go(&self) -> String {
self.qualified_expr("go.temporal.io/sdk/workflow", "workflow.Go")
}
pub(in crate::generator) fn new_nexus_client(&self) -> String {
self.qualified_expr("go.temporal.io/sdk/workflow", "workflow.NewNexusClient")
}
pub(in crate::generator) fn nexus_operation_options(&self) -> String {
self.qualified_expr(
"go.temporal.io/sdk/workflow",
"workflow.NexusOperationOptions{}",
)
}
fn qualified_expr(&self, import_path: &str, code_expr: &str) -> String {
if self.is_self_import(import_path) {
self.unqualify_self_expr(code_expr)
} else {
code_expr.to_string()
}
}
fn unqualify_self_expr(&self, code_expr: &str) -> String {
let qualifier = format!("{}.", self.package_name);
code_expr.replace(&qualifier, "")
}
}
struct GoGenerationResult {
files: GeneratedFileMap,
warnings: Vec<String>,
}
fn generate_in_tree(
api_plan: &PlannedSpec,
support_fragments: &[SupportFragmentSpec],
options: &GoOptions,
tree_models: &[PlannedJsonType],
primary_origin: GeneratedFileOrigin,
) -> Result<GoGenerationResult> {
ApiPlanner::new(api_plan, options, tree_models)?.generate(support_fragments, primary_origin)
}
fn collect_tree_json_models(leaves: &[&ApiSpecLeaf<PlannedFamily>]) -> Vec<PlannedJsonType> {
leaves
.iter()
.flat_map(|leaf| leaf.spec.external_types())
.filter_map(|(_, binding)| binding.json_model().cloned())
.collect()
}
pub(crate) fn generate_tree(
tree: &ApiSpecTree<PlannedFamily>,
support: &SupportFiles,
options: &GoOptions,
) -> Result<GeneratedFiles> {
match &tree.root {
ApiSpecNode::Leaf(leaf) => generate_single_leaf(leaf, support, options),
ApiSpecNode::Branch(branch) => generate_branch_tree(branch, support, options),
}
}
fn generate_single_leaf(
leaf: &ApiSpecLeaf<PlannedFamily>,
support: &SupportFiles,
options: &GoOptions,
) -> Result<GeneratedFiles> {
let primary_origin = if go_tree_has_json_models(&[leaf]) {
GeneratedFileOrigin::output_directory_named_module(Language::Go, &options.output_dir_name)
} else if let Some(service) = leaf.spec.services.first() {
GeneratedFileOrigin::service_declaration(Language::Go, &leaf.source_path, &service.name)
} else {
GeneratedFileOrigin::fixed("generated Go API module")
};
let mut generated =
generate_in_tree(&leaf.spec, &support.fragments, options, &[], primary_origin)?;
if go_tree_has_json_models(&[leaf]) {
let package_name = GoPackageContext::new(&leaf.spec, options)?.package_name;
let definitions_path = PathBuf::from("definitions.go");
let tree_models = collect_tree_json_models(&[leaf]);
generated.files.insert(
definitions_path,
json::render_definitions_file(&package_name, &tree_models)?,
GeneratedFileOrigin::fixed("generated Go validation runtime"),
)?;
}
Ok(GeneratedFiles {
layout: crate::generator::GeneratedOutputLayout::Directory,
files: generated.files.into_files(),
warnings: generated.warnings,
})
}
fn generate_branch_tree(
branch: &ApiSpecBranch<PlannedFamily>,
support: &SupportFiles,
options: &GoOptions,
) -> Result<GeneratedFiles> {
let mut leaves = Vec::new();
collect_leaf_specs(branch, &mut leaves);
if leaves.is_empty() {
return Err(Error::InvalidJsonSchema {
path: PathBuf::from("<go-tree-generator>"),
reason: "input tree contains no API specs".to_string(),
});
};
let root = ModulePath::default();
let package_name = GoPackageContext::new(&leaves[0].spec, options)?.package_name;
let tree_models = collect_tree_json_models(&leaves);
let mut files = GeneratedFileMap::default();
let mut warnings = Vec::new();
for leaf in &leaves {
let mut leaf_spec = leaf.spec.clone();
leaf_spec.module_path = root.clone();
let generated = generate_in_tree(
&leaf_spec,
&[],
options,
&tree_models,
GeneratedFileOrigin::input_module(Language::Go, &leaf.source_path),
)?;
warnings.extend(generated.warnings);
let file_name = go_flat_module_file_name(&leaf.module_path);
files.extend(generated.files.rekey_all(file_name)?)?;
}
if go_tree_has_json_models(&leaves) {
files.insert(
PathBuf::from("definitions.go"),
json::render_definitions_file(&package_name, &tree_models)?,
GeneratedFileOrigin::fixed("generated Go validation runtime"),
)?;
}
let support_fragments = support_fragments_for_plans(&leaves, support);
if !support_fragments.is_empty() {
files.insert(
PathBuf::from("support.go"),
render_support_file(&support_fragments, &package_name),
GeneratedFileOrigin::fixed("generated Go support file"),
)?;
}
Ok(GeneratedFiles {
layout: crate::generator::GeneratedOutputLayout::Directory,
files: files.into_files(),
warnings,
})
}
fn go_flat_module_file_name(module_path: &ModulePath) -> PathBuf {
let stem = if module_path.is_root() {
"api".to_string()
} else {
module_path.0.join("_")
};
PathBuf::from(format!("{stem}.go"))
}
fn go_tree_has_json_models(leaves: &[&ApiSpecLeaf<PlannedFamily>]) -> bool {
leaves.iter().any(|leaf| {
leaf.spec
.external_types()
.map(|(_, binding)| binding)
.any(|binding| binding.json_model().is_some())
})
}
fn support_fragments_for_plans(
plans: &[&ApiSpecLeaf<PlannedFamily>],
support: &SupportFiles,
) -> Vec<SupportFragmentSpec> {
if !support.fragments.is_empty() {
return support.fragments.clone();
}
plans
.iter()
.flat_map(|leaf| leaf.spec.support.fragments_for_language(Language::Go))
.cloned()
.collect()
}
fn collect_leaf_specs<'a>(
branch: &'a ApiSpecBranch<PlannedFamily>,
leaves: &mut Vec<&'a ApiSpecLeaf<PlannedFamily>>,
) {
for child in branch.children.values() {
match child {
ApiSpecNode::Leaf(leaf) => leaves.push(leaf),
ApiSpecNode::Branch(branch) => collect_leaf_specs(branch, leaves),
}
}
}
#[derive(Debug, Default)]
struct GoModelFragments {
imports: BTreeSet<String>,
body: String,
}
enum GoExternalModels {
Json(json::ModelBackend),
Proto(proto::ModelBackend),
}
fn is_json_planned_type(kind: &PlannedType) -> bool {
match kind {
TypeSpec::External(ExternalTypeSpec::Json(_)) => true,
TypeSpec::Option(inner) | TypeSpec::List(inner) => is_json_planned_type(inner),
_ => false,
}
}
fn plan_uses_json_models(api_plan: &PlannedSpec) -> bool {
api_plan
.external_types()
.any(|(_, binding)| binding.json_model().is_some())
|| api_plan
.services
.iter()
.flat_map(|service| &service.operations)
.any(|operation| {
[operation.input_type(), operation.output_type()]
.into_iter()
.flatten()
.any(is_json_planned_type)
})
}
impl GoExternalModels {
fn new(api_plan: &PlannedSpec, package: GoPackageContext) -> Self {
if plan_uses_json_models(api_plan) {
Self::Json(json::ModelBackend::new(
crate::nexgen_config::current().mode == GenerationMode::NativeApi,
))
} else {
Self::Proto(proto::ModelBackend::new(package))
}
}
fn adopt_tree_models(&mut self, tree_models: &[PlannedJsonType]) {
if let Self::Json(backend) = self {
backend.adopt_tree_models(tree_models);
}
}
fn prepare(&mut self, api_plan: &PlannedSpec) -> Result<()> {
match self {
Self::Json(backend) => backend.prepare(api_plan),
Self::Proto(backend) => backend.prepare(api_plan),
}
}
fn render_models(&self) -> Result<GoModelFragments> {
match self {
Self::Json(backend) => {
let fragments = backend.render_models()?;
Ok(GoModelFragments {
imports: fragments.imports,
body: fragments.body,
})
}
Self::Proto(backend) => {
let _ = backend.render_models()?;
Ok(GoModelFragments::default())
}
}
}
fn imports(&self) -> Vec<(String, String)> {
match self {
Self::Json(_) => Vec::new(),
Self::Proto(backend) => backend
.imports()
.paths()
.map(|(path, alias)| (path.clone(), alias.clone()))
.collect(),
}
}
fn model_type_annotation(&self, model_type: &PlannedValueType) -> Option<String> {
match self {
Self::Json(backend) => backend.model_type_annotation(model_type),
Self::Proto(backend) => backend.model_type_annotation(model_type),
}
}
fn has_message_wire_type(&self, message: &PlannedMessageType) -> bool {
match self {
Self::Json(backend) => backend.has_message_wire_type(message),
Self::Proto(backend) => backend.has_message_wire_type(message),
}
}
fn record_model(&mut self, key: &str, message: &PlannedMessageType) {
if let Self::Proto(backend) = self {
backend.record_model(key, message);
}
}
fn populate_model_wire_conversions(
&mut self,
api_plan: &PlannedSpec,
models: &IndexMap<String, RenderedModel>,
) -> Result<()> {
match self {
Self::Json(_) => Ok(()),
Self::Proto(backend) => backend.populate_model_wire_conversions(api_plan, models),
}
}
fn populate_operation_bindings(
&self,
api_plan: &PlannedSpec,
services: &mut [RenderedService<'_>],
) -> Result<()> {
match self {
Self::Json(_) => Ok(()),
Self::Proto(backend) => backend.populate_operation_bindings(api_plan, services),
}
}
fn render_model_wire_methods(&self, output: &mut String, key: &str, model: &RenderedModel) {
if let Self::Proto(backend) = self {
backend.render_model_wire_methods(output, key, model);
}
}
fn renders_operation_references(&self) -> bool {
matches!(self, Self::Json(_))
}
fn render_operation_references(
&self,
api_plan: &PlannedSpec,
package: &GoPackageContext,
) -> Result<Option<String>> {
match self {
Self::Json(backend) if backend.is_active() => {
Ok(Some(backend.render_services(api_plan, package)?))
}
_ => Ok(None),
}
}
}
struct ApiPlanner<'a> {
api_plan: &'a PlannedSpec,
package: GoPackageContext,
imports: BTreeSet<String>,
external_models: GoExternalModels,
enums: IndexMap<String, RenderedEnum>,
flags: IndexMap<String, RenderedFlags>,
variants: IndexMap<String, RenderedVariant>,
models: IndexMap<String, RenderedModel>,
}
impl<'a> ApiPlanner<'a> {
fn new(
api_plan: &'a PlannedSpec,
options: &GoOptions,
tree_models: &[PlannedJsonType],
) -> Result<Self> {
let package = GoPackageContext::new(api_plan, options)?;
let mut imports = BTreeSet::new();
collect_imports_from_plan(api_plan, &mut imports);
imports.retain(|import_path| !package.is_self_import(import_path));
let mut external_models = GoExternalModels::new(api_plan, package.clone());
external_models.adopt_tree_models(tree_models);
external_models.prepare(api_plan)?;
Ok(Self {
api_plan,
package,
imports,
external_models,
enums: IndexMap::new(),
flags: IndexMap::new(),
variants: IndexMap::new(),
models: IndexMap::new(),
})
}
fn generate(
mut self,
support_fragments: &[SupportFragmentSpec],
primary_origin: GeneratedFileOrigin,
) -> Result<GoGenerationResult> {
let mut services = Vec::new();
if !self.external_models.renders_operation_references() {
for service in &self.api_plan.services {
let mut operations = Vec::new();
for operation in &service.operations {
operations.push(self.resolve_operation(operation)?);
}
services.push(RenderedService {
wire_name: &service.wire_name,
endpoint: service
.endpoint
.clone()
.or_else(|| Some(service.name.to_kebab_case())),
operations,
resources: service
.resources
.iter()
.map(|resource| resource.data.clone())
.collect(),
});
}
}
for service in &services {
for resource in &service.resources {
self.ensure_resource_field_types(resource)?;
}
}
for record in self.api_plan.records().map(|(_, record)| record) {
self.ensure_rendered_model(&planned_message_type_for_record(record))?;
}
let visibility = compute_go_visibility(
self.api_plan,
&self.enums,
&self.flags,
&self.variants,
&self.models,
);
apply_go_visibility(
&visibility,
&mut self.enums,
&mut self.flags,
&mut self.variants,
&mut self.models,
&mut services,
);
self.populate_model_wire_conversions()?;
self.populate_operation_bindings(&mut services)?;
let needs_function_name_inlining = services.iter().any(service_uses_function_name_inlining);
let has_operations = services.iter().any(|s| !s.operations.is_empty());
if has_operations && !self.package.is_self_import("go.temporal.io/sdk/workflow") {
self.imports
.insert("go.temporal.io/sdk/workflow".to_string());
}
if services
.iter()
.flat_map(|service| &service.operations)
.any(|operation| {
operation
.wire_binding
.as_ref()
.is_some_and(|binding| binding.requires_fmt())
})
|| self.api_plan.records().any(|(_, record)| {
record.fields.values().any(|field| {
field
.flattened_annotation
.as_ref()
.and_then(|annotation| annotation.for_language(Language::Go))
.is_some()
})
})
{
self.imports.insert("fmt".to_string());
}
if needs_function_name_inlining {
self.imports.insert("reflect".to_string());
self.imports.insert("runtime".to_string());
self.imports.insert("strings".to_string());
}
let model_fragments = self.external_models.render_models()?;
self.imports.extend(model_fragments.imports);
if self.external_models.renders_operation_references()
&& !self.api_plan.services.is_empty()
&& !self
.package
.is_self_import("github.com/nexus-rpc/sdk-go/nexus")
{
self.imports
.insert("github.com/nexus-rpc/sdk-go/nexus".to_string());
}
if crate::nexgen_config::current().mode == GenerationMode::NativeApi
&& self.external_models.renders_operation_references()
&& !self.api_plan.services.is_empty()
&& !self.package.is_self_import("go.temporal.io/sdk/workflow")
{
self.imports
.insert("go.temporal.io/sdk/workflow".to_string());
}
let external_imports = self.external_models.imports();
let output = render_file(
&self.package,
&self.imports,
&external_imports,
&model_fragments.body,
self.external_models
.render_operation_references(self.api_plan, &self.package)?
.as_deref(),
self.enums.values().collect::<Vec<_>>().as_slice(),
self.flags.values().collect::<Vec<_>>().as_slice(),
self.variants.values().collect::<Vec<_>>().as_slice(),
self.models.iter().collect::<Vec<_>>().as_slice(),
&services,
&self.external_models,
self.api_plan,
&visibility,
);
let mut files = GeneratedFileMap::default();
let file_name = if self.external_models.renders_operation_references() {
PathBuf::from(format!("{}.go", self.package.package_name))
} else {
go_api_file_name(self.api_plan)
};
files.insert(file_name, output, primary_origin)?;
if !support_fragments.is_empty() {
files.insert(
PathBuf::from("support.go"),
render_support_file(support_fragments, &self.package.package_name),
GeneratedFileOrigin::fixed("generated Go support file"),
)?;
}
Ok(GoGenerationResult {
files,
warnings: Vec::new(),
})
}
fn resolve_operation(
&mut self,
operation: &'a PlannedOperation,
) -> Result<RenderedOperation<'a>> {
let input = operation
.input
.as_ref()
.and_then(|input| planned_message_type(input, self.api_plan))
.ok_or_else(|| Error::InvalidWit {
path: PathBuf::from("<go-plan>"),
reason: format!(
"operation `{}` has no proto-backed or WIT-native input type",
operation.name
),
})?;
let output = operation_output(operation, self.api_plan);
self.resolve_message_types(&input)?;
let model_type_parameters = record_for_message(self.api_plan, &input)
.map(|record| {
self.api_plan
.record_type_parameters(&record.full_name, Language::Go)
})
.unwrap_or_default();
let input_type = go_generic_reference(
&resolve_message_go_type(&input, &self.external_models),
&model_type_parameters,
false,
);
let go_output_transform = operation.output_transform.as_ref().and_then(|transform| {
Some((
transform.type_name.for_language(Language::Go)?,
transform.transform.for_language(Language::Go)?,
))
});
let raw_output_type = if go_output_transform.is_some() {
match &output {
PlannedOperationOutput::Message(output) => {
if self.external_models.has_message_wire_type(output) {
None
} else {
self.resolve_message_types(output)?;
Some(resolve_message_go_type(output, &self.external_models))
}
}
PlannedOperationOutput::Resource { type_name } => Some(type_name.clone()),
PlannedOperationOutput::None => None,
}
} else {
None
};
let output_type = match &output {
PlannedOperationOutput::Message(output) => {
if let Some(resource_return) = &operation.data.output_resource_return {
Some(resource_return.resource_type_name.clone())
} else if let Some((transform_type, _)) = go_output_transform {
Some(self.package.go_type_expr(transform_type))
} else {
self.resolve_message_types(output)?;
Some(resolve_message_go_type(output, &self.external_models))
}
}
PlannedOperationOutput::Resource { type_name } => Some(type_name.clone()),
PlannedOperationOutput::None => None,
};
let unpacked_input = self.models.get(&input.info.full_name).map(|model| {
let planned_record = record_for_message(self.api_plan, &input);
let planned_fields = planned_record.map(|record| {
record
.public_fields()
.map(|(field_name, field)| {
planned_field(record, field_name, field, self.api_plan)
})
.collect::<Vec<_>>()
});
model
.fields
.iter()
.enumerate()
.filter_map(|(index, field)| {
let planned_field =
planned_fields.as_ref().and_then(|fields| fields.get(index));
let planned_field = planned_field?;
let function = match &planned_field.role {
PlannedFieldRole::Function(function) if planned_field.required => {
Some(function.clone())
}
_ => None,
};
let function_args = match &planned_field.role {
PlannedFieldRole::FunctionArgs(function) => Some(function.clone()),
_ => None,
};
let embed_in_options = !planned_field.required
&& field_kind_is_flattened_message(&planned_field.kind, self.api_plan);
let default_punning_zero = (!planned_field.required && !embed_in_options)
.then(|| {
public_default_punning_zero_for_field(
&planned_field.kind,
&field.go_type,
)
})
.flatten();
let public_go_type = if default_punning_zero.is_some() {
field
.go_type
.strip_prefix('*')
.unwrap_or(&field.go_type)
.to_string()
} else {
field.go_type.clone()
};
Some(RenderedUnpackedParam {
field_name: field.name.clone(),
doc: field.doc.clone(),
param_name: go_unexported_name(&field.name),
go_type: field.go_type.clone(),
public_go_type,
default_punning_zero,
function,
function_args,
required: field.required,
embed_in_options,
})
})
.collect()
});
Ok(RenderedOperation {
name: operation.name.as_str(),
wire_name: operation.wire_name.as_str(),
doc: operation.doc.for_language(Language::Go),
return_doc: operation.return_doc.for_language(Language::Go),
func_name: go_unexported_name(&operation.name),
input_type,
model_type_parameters,
output_type,
raw_output_type,
output_transform_expr: go_output_transform.map(|(_, expr)| expr),
output_transform_type: go_output_transform
.map(|(type_name, _)| self.package.go_type_expr(type_name)),
unpacked_input,
wire_binding: None,
})
}
fn ensure_resource_field_types(&mut self, resource: &PlannedResource) -> Result<()> {
for field in &resource.fields {
self.ensure_resource_field_type(&planned_field_kind(&field.kind, self.api_plan))?;
}
for method in &resource.methods {
for param in &method.params {
self.ensure_resource_field_type(&planned_field_kind(¶m.kind, self.api_plan))?;
}
}
Ok(())
}
fn ensure_resource_field_type(&mut self, kind: &PlannedFieldKind) -> Result<()> {
match kind {
PlannedFieldKind::Singular(value) | PlannedFieldKind::Repeated(value) => {
self.resolve_planned_value_type(value)?;
}
PlannedFieldKind::Map { key, value } => {
self.resolve_planned_value_type(key)?;
self.resolve_planned_value_type(value)?;
}
}
Ok(())
}
fn resolve_message_types(&mut self, message: &PlannedMessageType) -> Result<()> {
if message.replacement.is_some() || message.authored_type.is_some() {
return Ok(());
}
if message.source == PlannedMessageSource::Json {
return Ok(());
}
self.ensure_rendered_model(message)
}
fn resolve_planned_value_type(
&mut self,
value_type: &PlannedValueType,
) -> Result<ResolvedGoType> {
match value_type {
PlannedValueType::TypeParameter(name) => Ok(ResolvedGoType {
type_expr: name.clone(),
}),
PlannedValueType::Scalar(PlannedScalarType::Float) => Ok(ResolvedGoType {
type_expr: "float64".to_string(),
}),
PlannedValueType::Scalar(PlannedScalarType::Int32) => Ok(ResolvedGoType {
type_expr: "int32".to_string(),
}),
PlannedValueType::Scalar(PlannedScalarType::Int64) => Ok(ResolvedGoType {
type_expr: "int64".to_string(),
}),
PlannedValueType::Scalar(PlannedScalarType::Bool) => Ok(ResolvedGoType {
type_expr: "bool".to_string(),
}),
PlannedValueType::Scalar(PlannedScalarType::String) => Ok(ResolvedGoType {
type_expr: "string".to_string(),
}),
PlannedValueType::Scalar(PlannedScalarType::Bytes) => Ok(ResolvedGoType {
type_expr: "[]byte".to_string(),
}),
PlannedValueType::Enum(enum_type) => {
let backend_type = self.external_models.model_type_annotation(value_type);
if let (Some(info), Some(name)) = (&enum_type.info, &enum_type.name) {
if let Some(enum_decl) = self.api_plan.enum_decl(&info.full_name) {
self.ensure_rendered_enum(&planned_enum(enum_decl));
} else if enum_type.replacement.is_none() {
self.ensure_rendered_enum(&PlannedEnum {
info: info.clone(),
name: name.clone(),
values: Vec::new(),
});
}
Ok(ResolvedGoType {
type_expr: backend_type.unwrap_or_else(|| name.clone()),
})
} else {
Ok(ResolvedGoType {
type_expr: backend_type.unwrap_or_else(|| "int32".to_string()),
})
}
}
PlannedValueType::Flags(flags_type) => {
if let Some(flags_decl) = self.api_plan.flags_decl(&flags_type.info.full_name) {
self.ensure_rendered_flags(&planned_flags(flags_decl));
}
Ok(ResolvedGoType {
type_expr: flags_type.name.clone(),
})
}
PlannedValueType::Variant(variant_type) => {
if let Some(variant_decl) = self.api_plan.variant(&variant_type.info.full_name) {
let planned = planned_variant(variant_decl, self.api_plan);
self.ensure_rendered_variant(&planned)?;
}
Ok(ResolvedGoType {
type_expr: go_generic_reference(
&variant_type.name,
&self
.api_plan
.variant_type_parameters(&variant_type.info.full_name, Language::Go),
false,
),
})
}
PlannedValueType::Message(message_type) => {
if let Some(type_name) = self.external_models.model_type_annotation(value_type)
&& (message_type.replacement.is_some() || message_type.authored_type.is_some())
{
return Ok(ResolvedGoType {
type_expr: type_name,
});
}
self.ensure_rendered_model(message_type)?;
let base = self
.external_models
.model_type_annotation(value_type)
.unwrap_or_else(|| message_type.model_name.clone());
let type_expr = record_for_message(self.api_plan, message_type)
.map(|record| {
go_generic_reference(
&base,
&self
.api_plan
.record_type_parameters(&record.full_name, Language::Go),
false,
)
})
.unwrap_or(base);
Ok(ResolvedGoType { type_expr })
}
PlannedValueType::Tuple(items) => {
let type_name = self.ensure_generic_tuple(items.len())?;
let args = items
.iter()
.map(|item| {
self.resolve_planned_value_type(item)
.map(|resolved| resolved.type_expr)
})
.collect::<Result<Vec<_>>>()?;
Ok(ResolvedGoType {
type_expr: format!("{type_name}[{}]", args.join(", ")),
})
}
PlannedValueType::Result { ok, err } => {
self.ensure_generic_result();
let ok_type = match ok {
Some(ok) => self.resolve_planned_value_type(ok)?.type_expr,
None => "struct{}".to_string(),
};
let err_type = match err {
Some(err) => self.resolve_planned_value_type(err)?.type_expr,
None => "error".to_string(),
};
Ok(ResolvedGoType {
type_expr: format!("Result[{ok_type}, {err_type}]"),
})
}
PlannedValueType::External {
type_name,
fallback,
} => {
if let Some(annotation) = type_name.for_language(Language::Go) {
Ok(ResolvedGoType {
type_expr: self.package.go_type_expr(annotation),
})
} else {
self.resolve_planned_value_type(fallback)
}
}
PlannedValueType::Unknown => Ok(ResolvedGoType {
type_expr: "any".to_string(),
}),
}
}
fn ensure_rendered_enum(&mut self, planned_enum: &PlannedEnum) {
self.enums
.entry(planned_enum.info.full_name.clone())
.or_insert_with(|| RenderedEnum {
name: planned_enum.name.clone(),
values: planned_enum
.values
.iter()
.map(|value| RenderedEnumValue {
name: value.name.clone(),
number: value.number,
})
.collect(),
});
}
fn ensure_rendered_flags(&mut self, planned_flags: &PlannedFlags) {
self.flags
.entry(planned_flags.info.full_name.clone())
.or_insert_with(|| RenderedFlags {
name: planned_flags.name.clone(),
flags: planned_flags
.flags
.iter()
.map(|flag| RenderedFlag {
name: flag.name.clone(),
bit: flag.bit,
})
.collect(),
});
}
fn ensure_rendered_variant(&mut self, planned_variant: &PlannedVariant) -> Result<()> {
if self.variants.contains_key(&planned_variant.info.full_name) {
return Ok(());
}
let variant_name = &planned_variant.name;
let mut cases = Vec::new();
for case in &planned_variant.cases {
let case_suffix = go_field_name(&case.name);
let payload_type = case
.payload
.as_ref()
.map(|payload| {
self.resolve_planned_value_type(payload)
.map(|resolved| resolved.type_expr)
})
.transpose()?;
cases.push(RenderedVariantCase {
struct_name: format!("{variant_name}{case_suffix}"),
payload_type,
});
}
self.variants.insert(
planned_variant.info.full_name.clone(),
RenderedVariant {
name: variant_name.clone(),
type_parameters: planned_variant.type_parameters.clone(),
marker_method: format!("is{variant_name}"),
cases,
},
);
Ok(())
}
fn ensure_rendered_model(&mut self, message: &PlannedMessageType) -> Result<()> {
let Some((model_name, model_parameters, planned_fields)) =
record_for_message(self.api_plan, message).map(|planned_model| {
let parameters = self
.api_plan
.record_type_parameters(&planned_model.full_name, Language::Go);
let planned_fields = planned_model
.model_fields()
.map(|(field_name, field)| {
planned_field(planned_model, field_name, field, self.api_plan)
})
.collect::<Vec<_>>();
(planned_model.name.clone(), parameters, planned_fields)
})
else {
if !self.models.contains_key(&message.info.full_name) {
self.models.insert(
message.info.full_name.clone(),
RenderedModel {
name: message.model_name.clone(),
fields: Vec::new(),
},
);
self.external_models
.record_model(&message.info.full_name, message);
}
return Ok(());
};
if self.models.contains_key(&message.info.full_name) {
return Ok(());
}
self.models.insert(
message.info.full_name.clone(),
RenderedModel {
name: go_generic_declaration_name(&model_name, &model_parameters),
fields: Vec::new(),
},
);
self.external_models
.record_model(&message.info.full_name, message);
let fields = planned_fields
.iter()
.map(|planned_field| self.build_field(planned_field))
.collect::<Result<Vec<_>>>()?;
self.models
.get_mut(&message.info.full_name)
.expect("model should be inserted before recursive field resolution")
.fields = fields;
Ok(())
}
fn build_field(&mut self, field: &PlannedField) -> Result<RenderedField> {
let field_name = go_field_name(&field.authored_name);
let annotated_go_type = field
.flattened_annotation_override
.as_ref()
.or(field.annotation_override.as_ref())
.and_then(|annotation| annotation.for_language(Language::Go))
.map(|annotation| self.package.go_type_expr(annotation));
let go_type = if let Some(annotated) = annotated_go_type {
annotated
} else {
match &field.kind {
PlannedFieldKind::Map { key, value } => {
let key_type = self.resolve_planned_value_type(key)?;
let value_type = self.resolve_planned_value_type(value)?;
format!("map[{}]{}", key_type.type_expr, value_type.type_expr)
}
PlannedFieldKind::Repeated(value) => {
let element_type = self.resolve_planned_value_type(value)?;
format!("[]{}", element_type.type_expr)
}
PlannedFieldKind::Singular(value) => {
let resolved = self.resolve_planned_value_type(value)?;
if !field.required
&& type_needs_pointer_when_optional(value, &resolved.type_expr)
{
format!("*{}", resolved.type_expr)
} else {
resolved.type_expr
}
}
}
};
Ok(RenderedField {
name: field_name,
doc: field
.doc
.as_ref()
.and_then(|doc| doc.for_language(Language::Go))
.map(str::to_string),
go_type,
required: field.required,
})
}
fn ensure_generic_tuple(&mut self, arity: usize) -> Result<String> {
ensure_generic_tuple(arity, &mut self.models)
}
fn ensure_generic_result(&mut self) {
ensure_generic_result(&mut self.models);
}
fn populate_model_wire_conversions(&mut self) -> Result<()> {
self.external_models
.populate_model_wire_conversions(self.api_plan, &self.models)
}
fn populate_operation_bindings(&self, services: &mut [RenderedService<'_>]) -> Result<()> {
self.external_models
.populate_operation_bindings(self.api_plan, services)
}
}
fn service_uses_function_name_inlining(service: &RenderedService<'_>) -> bool {
service.operations.iter().any(|operation| {
operation.unpacked_input.as_ref().is_some_and(|params| {
params
.iter()
.any(|param| param.required && param.function.is_some())
})
})
}
fn go_api_file_name(api_plan: &PlannedSpec) -> PathBuf {
let stem = api_plan
.services
.first()
.map(|service| {
service
.name
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect::<String>()
.to_lowercase()
})
.filter(|name| !name.is_empty())
.unwrap_or_else(|| "api".to_string());
PathBuf::from(format!("{stem}.go"))
}
fn render_support_file(fragments: &[SupportFragmentSpec], package_name: &str) -> String {
if let [fragment] = fragments {
return rewrite_support_package(&fragment.contents, package_name);
}
let mut prelude = String::new();
let mut imports = BTreeSet::new();
let mut bodies = Vec::new();
for fragment in fragments {
let parsed = parse_support_fragment(&fragment.contents);
if prelude.is_empty() {
prelude = parsed.prelude;
} else if !parsed.prelude.trim().is_empty() {
bodies.push(parsed.prelude.trim().to_string());
}
imports.extend(parsed.imports);
if !parsed.body.trim().is_empty() {
bodies.push(parsed.body.trim().to_string());
}
}
let mut output = String::new();
if !prelude.trim().is_empty() {
output.push_str(prelude.trim_end());
output.push('\n');
}
output.push_str("package ");
output.push_str(package_name);
output.push_str("\n\n");
if !imports.is_empty() {
output.push_str("import (\n");
for import in imports {
output.push('\t');
output.push_str(&import);
output.push('\n');
}
output.push_str(")\n\n");
}
output.push_str(&bodies.join("\n\n"));
output.push('\n');
output
}
fn rewrite_support_package(contents: &str, package_name: &str) -> String {
let mut result = String::with_capacity(contents.len());
let mut replaced = false;
for line in contents.lines() {
if !replaced && line.trim_start().starts_with("package ") {
result.push_str("package ");
result.push_str(package_name);
replaced = true;
} else {
result.push_str(line);
}
result.push('\n');
}
result
}
#[derive(Default)]
struct ParsedSupportFragment {
prelude: String,
imports: BTreeSet<String>,
body: String,
}
fn parse_support_fragment(contents: &str) -> ParsedSupportFragment {
let lines = contents.lines().collect::<Vec<_>>();
let Some(package_index) = lines
.iter()
.position(|line| line.trim_start().starts_with("package "))
else {
return ParsedSupportFragment {
body: contents.to_string(),
..Default::default()
};
};
let mut parsed = ParsedSupportFragment {
prelude: lines[..package_index].join("\n"),
..Default::default()
};
let mut index = package_index + 1;
skip_blank_lines(&lines, &mut index);
while index < lines.len() {
let line = lines[index].trim_start();
if line == "import (" {
index += 1;
while index < lines.len() && lines[index].trim() != ")" {
let import = lines[index].trim();
if !import.is_empty() {
parsed.imports.insert(import.to_string());
}
index += 1;
}
if index < lines.len() {
index += 1;
}
} else if let Some(import) = line.strip_prefix("import ") {
parsed.imports.insert(import.trim().to_string());
index += 1;
} else {
break;
}
skip_blank_lines(&lines, &mut index);
}
parsed.body = lines[index..].join("\n");
parsed
}
fn skip_blank_lines(lines: &[&str], index: &mut usize) {
while *index < lines.len() && lines[*index].trim().is_empty() {
*index += 1;
}
}
pub(crate) fn go_package_name(endpoint: &str) -> String {
endpoint
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '_')
.collect::<String>()
.to_lowercase()
}
#[derive(Debug)]
pub(in crate::generator) struct RenderedService<'a> {
pub(in crate::generator) wire_name: &'a str,
pub(in crate::generator) endpoint: Option<String>,
pub(in crate::generator) operations: Vec<RenderedOperation<'a>>,
pub(in crate::generator) resources: Vec<PlannedResource>,
}
#[derive(Debug)]
pub(in crate::generator) struct RenderedOperation<'a> {
name: &'a str,
pub(in crate::generator) wire_name: &'a str,
doc: Option<&'a str>,
return_doc: Option<&'a str>,
pub(in crate::generator) func_name: String,
pub(in crate::generator) input_type: String,
model_type_parameters: Vec<crate::spec::TypeParameterUsage>,
pub(in crate::generator) output_type: Option<String>,
pub(in crate::generator) raw_output_type: Option<String>,
pub(in crate::generator) output_transform_expr: Option<&'a str>,
pub(in crate::generator) output_transform_type: Option<String>,
unpacked_input: Option<Vec<RenderedUnpackedParam>>,
pub(in crate::generator) wire_binding: Option<proto::OperationBinding>,
}
#[derive(Debug)]
struct RenderedUnpackedParam {
field_name: String,
doc: Option<String>,
param_name: String,
go_type: String,
public_go_type: String,
default_punning_zero: Option<String>,
function: Option<FunctionFieldSpec>,
function_args: Option<FunctionFieldSpec>,
required: bool,
embed_in_options: bool,
}
#[derive(Debug)]
struct RenderedEnum {
name: String,
values: Vec<RenderedEnumValue>,
}
#[derive(Debug)]
struct RenderedEnumValue {
name: String,
number: i32,
}
#[derive(Debug)]
struct RenderedFlags {
name: String,
flags: Vec<RenderedFlag>,
}
#[derive(Debug)]
struct RenderedFlag {
name: String,
bit: usize,
}
#[derive(Debug)]
struct RenderedVariant {
name: String,
type_parameters: Vec<crate::spec::TypeParameterUsage>,
marker_method: String,
cases: Vec<RenderedVariantCase>,
}
#[derive(Debug)]
struct RenderedVariantCase {
struct_name: String,
payload_type: Option<String>,
}
#[derive(Debug)]
pub(in crate::generator) struct RenderedModel {
pub(in crate::generator) name: String,
pub(in crate::generator) fields: Vec<RenderedField>,
}
#[derive(Debug)]
pub(in crate::generator) struct RenderedField {
pub(in crate::generator) name: String,
pub(in crate::generator) doc: Option<String>,
pub(in crate::generator) go_type: String,
pub(in crate::generator) required: bool,
}
#[derive(Debug, Clone)]
struct ResolvedGoType {
type_expr: String,
}
#[derive(Debug, Clone, Default)]
struct GoVisibility {
public_keys: BTreeSet<String>,
type_name_replacements: BTreeMap<String, String>,
}
impl GoVisibility {
fn is_public(&self, key: &str) -> bool {
self.public_keys.contains(key)
}
fn adjusted_model_name(&self, key: &str, fallback: &str) -> String {
self.type_name_replacements
.get(fallback)
.cloned()
.unwrap_or_else(|| {
if self.is_public(key) {
fallback.to_string()
} else {
go_unexported_name(fallback)
}
})
}
fn rewrite_go_expr(&self, expr: &str) -> String {
if self.type_name_replacements.is_empty() && !expr.contains(".ToProto()") {
return expr.to_string();
}
let expr = expr.replace(".ToProto()", ".toProto()");
let bytes = expr.as_bytes();
let mut output = String::with_capacity(expr.len());
let mut index = 0;
while index < bytes.len() {
let byte = bytes[index];
if is_go_ident_start(byte) {
let start = index;
index += 1;
while index < bytes.len() && is_go_ident_continue(bytes[index]) {
index += 1;
}
let ident = &expr[start..index];
let prev = previous_non_whitespace(bytes, start);
let next = next_non_whitespace(bytes, index);
if prev == Some(b'.') || next == Some(b':') {
output.push_str(ident);
} else if let Some(replacement) = self.type_name_replacements.get(ident) {
output.push_str(replacement);
} else {
output.push_str(ident);
}
} else {
output.push(byte as char);
index += 1;
}
}
output
}
}
fn is_go_ident_start(byte: u8) -> bool {
byte == b'_' || byte.is_ascii_alphabetic()
}
fn is_go_ident_continue(byte: u8) -> bool {
is_go_ident_start(byte) || byte.is_ascii_digit()
}
fn previous_non_whitespace(bytes: &[u8], start: usize) -> Option<u8> {
bytes[..start]
.iter()
.rev()
.copied()
.find(|byte| !byte.is_ascii_whitespace())
}
fn next_non_whitespace(bytes: &[u8], start: usize) -> Option<u8> {
bytes[start..]
.iter()
.copied()
.find(|byte| !byte.is_ascii_whitespace())
}
fn compute_go_visibility(
api_plan: &PlannedSpec,
enums: &IndexMap<String, RenderedEnum>,
flags: &IndexMap<String, RenderedFlags>,
variants: &IndexMap<String, RenderedVariant>,
models: &IndexMap<String, RenderedModel>,
) -> GoVisibility {
let mut marker = GoVisibilityMarker {
api_plan,
public_keys: BTreeSet::new(),
};
for service in &api_plan.services {
for operation in &service.operations {
marker.mark_operation_public_surface(operation);
}
for resource in &service.resources {
marker.mark_resource_public_surface(&resource.data);
}
}
let mut visibility = GoVisibility {
public_keys: marker.public_keys,
type_name_replacements: BTreeMap::new(),
};
for (key, enumeration) in enums {
if !visibility.is_public(key) {
visibility.type_name_replacements.insert(
enumeration.name.clone(),
go_unexported_name(&enumeration.name),
);
}
}
for (key, flag_set) in flags {
if !visibility.is_public(key) {
visibility
.type_name_replacements
.insert(flag_set.name.clone(), go_unexported_name(&flag_set.name));
}
}
for (key, variant) in variants {
if !visibility.is_public(key) {
visibility
.type_name_replacements
.insert(variant.name.clone(), go_unexported_name(&variant.name));
for case in &variant.cases {
visibility.type_name_replacements.insert(
case.struct_name.clone(),
go_unexported_name(&case.struct_name),
);
}
}
}
for (key, model) in models {
if !visibility.is_public(key) {
let (ident, _) = split_go_type_decl_name(&model.name);
visibility
.type_name_replacements
.insert(ident.to_string(), go_unexported_name(ident));
}
}
visibility
}
struct GoVisibilityMarker<'a> {
api_plan: &'a PlannedSpec,
public_keys: BTreeSet<String>,
}
impl GoVisibilityMarker<'_> {
fn mark_operation_public_surface(&mut self, operation: &PlannedOperation) {
if let Some(input) = operation
.input
.as_ref()
.and_then(|input| planned_message_type(input, self.api_plan))
&& let Some(planned_model) = record_for_message(self.api_plan, &input)
{
for (_, field) in planned_model.public_fields() {
self.mark_operation_field_kind(
&planned_field_kind(&field.field_type, self.api_plan),
field.name.as_str(),
);
}
}
if operation
.output_transform
.as_ref()
.is_some_and(|transform| {
transform.type_name.for_language(Language::Go).is_some()
&& transform.transform.for_language(Language::Go).is_some()
})
{
return;
}
match operation_output(operation, self.api_plan) {
PlannedOperationOutput::Message(output) => {
if operation.data.output_resource_return.is_none() {
self.mark_message_type(&output);
}
}
PlannedOperationOutput::Resource { .. } | PlannedOperationOutput::None => {}
}
}
fn mark_resource_public_surface(&mut self, resource: &PlannedResource) {
for field in &resource.fields {
self.mark_resource_field_kind(&planned_field_kind(&field.kind, self.api_plan));
}
for method in &resource.methods {
for param in &method.params {
self.mark_resource_field_kind(&planned_field_kind(¶m.kind, self.api_plan));
}
if let Some(result) = &method.result
&& let PlannedResourceMethodResult::Value(kind) = &result.kind
{
self.mark_resource_field_kind(&planned_field_kind(kind, self.api_plan));
}
}
}
fn mark_message_type(&mut self, message: &PlannedMessageType) {
if message.replacement.is_some() || message.authored_type.is_some() {
return;
}
if !self.public_keys.insert(message.info.full_name.clone()) {
return;
}
if let Some(model) = record_for_message(self.api_plan, message) {
for (_, field) in model.public_fields() {
self.mark_operation_field_kind(
&planned_field_kind(&field.field_type, self.api_plan),
field.name.as_str(),
);
}
}
}
fn mark_operation_field_kind(&mut self, kind: &PlannedFieldKind, field_name: &str) {
match kind {
PlannedFieldKind::Singular(PlannedValueType::Tuple(items)) => {
self.public_keys.insert(go_field_name(field_name));
for item in items {
self.mark_value_type(item);
}
}
PlannedFieldKind::Singular(PlannedValueType::Result { ok, err }) => {
self.public_keys.insert(go_field_name(field_name));
if let Some(ok) = ok.as_deref() {
self.mark_value_type(ok);
}
if let Some(err) = err.as_deref() {
self.mark_value_type(err);
}
}
PlannedFieldKind::Singular(value) | PlannedFieldKind::Repeated(value) => {
self.mark_value_type(value);
}
PlannedFieldKind::Map { key, value } => {
self.mark_value_type(key);
self.mark_value_type(value);
}
}
}
fn mark_resource_field_kind(&mut self, kind: &PlannedFieldKind) {
match kind {
PlannedFieldKind::Singular(value) | PlannedFieldKind::Repeated(value) => {
self.mark_value_type(value);
}
PlannedFieldKind::Map { key, value } => {
self.mark_value_type(key);
self.mark_value_type(value);
}
}
}
fn mark_value_type(&mut self, value: &PlannedValueType) {
match value {
PlannedValueType::TypeParameter(_)
| PlannedValueType::Scalar(_)
| PlannedValueType::Unknown => {}
PlannedValueType::External {
type_name,
fallback,
} => {
if type_name.for_language(Language::Go).is_none() {
self.mark_value_type(fallback);
}
}
PlannedValueType::Enum(enum_type) => {
if enum_type.replacement.is_some() {
return;
}
if let Some(info) = &enum_type.info {
self.public_keys.insert(info.full_name.clone());
}
}
PlannedValueType::Flags(flags_type) => {
self.public_keys.insert(flags_type.info.full_name.clone());
}
PlannedValueType::Variant(variant_type) => {
if !self.public_keys.insert(variant_type.info.full_name.clone()) {
return;
}
if let Some(variant) = self.api_plan.variant(&variant_type.info.full_name) {
for case in &variant.cases {
if let Some(payload) = &case.payload {
self.mark_value_type(&planned_value_type(payload, self.api_plan));
}
}
}
}
PlannedValueType::Message(message_type) => self.mark_message_type(message_type),
PlannedValueType::Tuple(items) => {
self.public_keys
.insert(format!("{GENERIC_MODEL_KEY_PREFIX}Tuple{}", items.len()));
for item in items {
self.mark_value_type(item);
}
}
PlannedValueType::Result { ok, err } => {
self.public_keys
.insert(format!("{GENERIC_MODEL_KEY_PREFIX}Result"));
if let Some(ok) = ok.as_deref() {
self.mark_value_type(ok);
}
if let Some(err) = err.as_deref() {
self.mark_value_type(err);
}
}
}
}
}
fn apply_go_visibility(
visibility: &GoVisibility,
enums: &mut IndexMap<String, RenderedEnum>,
flags: &mut IndexMap<String, RenderedFlags>,
variants: &mut IndexMap<String, RenderedVariant>,
models: &mut IndexMap<String, RenderedModel>,
services: &mut [RenderedService<'_>],
) {
for enumeration in enums.values_mut() {
enumeration.name = visibility.rewrite_go_expr(&enumeration.name);
}
for flag_set in flags.values_mut() {
flag_set.name = visibility.rewrite_go_expr(&flag_set.name);
}
for variant in variants.values_mut() {
variant.name = visibility.rewrite_go_expr(&variant.name);
for case in &mut variant.cases {
case.struct_name = visibility.rewrite_go_expr(&case.struct_name);
if let Some(payload_type) = &mut case.payload_type {
*payload_type = visibility.rewrite_go_expr(payload_type);
}
}
}
for model in models.values_mut() {
model.name = visibility.rewrite_go_type_decl(&model.name);
for field in &mut model.fields {
field.go_type = visibility.rewrite_go_expr(&field.go_type);
}
}
for service in services {
for operation in &mut service.operations {
operation.input_type = visibility.rewrite_go_expr(&operation.input_type);
if let Some(output_type) = &mut operation.output_type {
*output_type = visibility.rewrite_go_expr(output_type);
}
if let Some(raw_output_type) = &mut operation.raw_output_type {
*raw_output_type = visibility.rewrite_go_expr(raw_output_type);
}
if let Some(output_transform_type) = &mut operation.output_transform_type {
*output_transform_type = visibility.rewrite_go_expr(output_transform_type);
}
if let Some(params) = &mut operation.unpacked_input {
for param in params {
param.go_type = visibility.rewrite_go_expr(¶m.go_type);
}
}
}
}
}
impl GoVisibility {
fn rewrite_go_type_decl(&self, decl: &str) -> String {
let (ident, suffix) = split_go_type_decl_name(decl);
match self.type_name_replacements.get(ident) {
Some(replacement) => format!("{replacement}{suffix}"),
None => decl.to_string(),
}
}
}
pub(in crate::generator) fn split_go_type_decl_name(decl: &str) -> (&str, &str) {
decl.find('[')
.map(|index| (&decl[..index], &decl[index..]))
.unwrap_or((decl, ""))
}
fn is_go_exported_type_decl(decl: &str) -> bool {
let (ident, _) = split_go_type_decl_name(decl);
is_go_exported_decl(ident)
}
fn is_go_exported_decl(name: &str) -> bool {
name.chars()
.next()
.is_some_and(|first| first.is_ascii_uppercase())
}
pub(in crate::generator) fn go_string_literal(value: &str) -> String {
format!("{value:?}")
}
fn field_kind_is_flattened_message(kind: &PlannedFieldKind, api_plan: &PlannedSpec) -> bool {
let PlannedFieldKind::Singular(PlannedValueType::Message(message)) = kind else {
return false;
};
record_for_message(api_plan, message).is_some_and(|model| model.flatten_in_api)
}
pub(in crate::generator) fn public_default_punning_zero_for_field(
kind: &PlannedFieldKind,
internal_go_type: &str,
) -> Option<String> {
let public_go_type = internal_go_type.strip_prefix('*')?;
if let Some(zero) = public_default_punning_zero_for_go_type(public_go_type) {
return Some(zero.to_string());
}
match kind {
PlannedFieldKind::Singular(value) => {
public_default_punning_zero_for_value(value, public_go_type)
}
PlannedFieldKind::Repeated(_) | PlannedFieldKind::Map { .. } => None,
}
}
fn public_default_punning_zero_for_value(
value: &PlannedValueType,
public_go_type: &str,
) -> Option<String> {
match value {
PlannedValueType::Scalar(scalar) => match scalar {
PlannedScalarType::Bool => Some("false".to_string()),
PlannedScalarType::String => Some("\"\"".to_string()),
PlannedScalarType::Int32 | PlannedScalarType::Int64 | PlannedScalarType::Float => {
Some("0".to_string())
}
PlannedScalarType::Bytes => None,
},
PlannedValueType::Enum(_) | PlannedValueType::Flags(_) => Some("0".to_string()),
PlannedValueType::External { .. } => {
public_default_punning_zero_for_go_type(public_go_type).map(str::to_string)
}
PlannedValueType::TypeParameter(_)
| PlannedValueType::Variant(_)
| PlannedValueType::Message(_)
| PlannedValueType::Tuple(_)
| PlannedValueType::Result { .. }
| PlannedValueType::Unknown => None,
}
}
fn public_default_punning_zero_for_go_type(public_go_type: &str) -> Option<&'static str> {
match public_go_type {
"string" => Some("\"\""),
"bool" => Some("false"),
"int" | "int8" | "int16" | "int32" | "int64" | "uint" | "uint8" | "uint16" | "uint32"
| "uint64" | "float32" | "float64" | "time.Duration" => Some("0"),
_ => None,
}
}
fn resolve_message_go_type(
message: &PlannedMessageType,
external_models: &GoExternalModels,
) -> String {
external_models
.model_type_annotation(&PlannedValueType::Message(message.clone()))
.unwrap_or_else(|| message.model_name.clone())
}
pub(in crate::generator) fn go_replacement_type_name(
replacement: &TypeReplacementSpec,
package: &GoPackageContext,
) -> Option<String> {
replacement
.type_name
.for_language(Language::Go)
.map(|annotation| package.go_type_expr(annotation))
}
pub(in crate::generator) fn go_authored_type_annotation(wit_type: &PlannedType) -> String {
match wit_type {
TypeSpec::Bool => "bool".to_string(),
TypeSpec::Int(IntSpec::I32) => "int32".to_string(),
TypeSpec::Int(IntSpec::I64) => "int64".to_string(),
TypeSpec::Float => "float64".to_string(),
TypeSpec::String => "string".to_string(),
TypeSpec::Bytes => "[]byte".to_string(),
TypeSpec::TypeParameter(parameter) => parameter.name.clone(),
TypeSpec::Option(inner) => format!("*{}", go_authored_type_annotation(inner)),
TypeSpec::List(inner) => format!("[]{}", go_authored_type_annotation(inner)),
TypeSpec::Tuple(_) => "any".to_string(),
TypeSpec::Map(key, value) => format!(
"map[{}]{}",
go_authored_type_annotation(key),
go_authored_type_annotation(value)
),
TypeSpec::Result { .. } => "any".to_string(),
TypeSpec::External(ExternalTypeSpec::Alias(AliasTypeSpec {
type_name, target, ..
})) => type_name
.for_language(Language::Go)
.map(str::to_string)
.unwrap_or_else(|| go_authored_type_annotation(target)),
TypeSpec::External(ExternalTypeSpec::Proto(PlannedProtoType::Message(message))) => {
message.model_name.clone()
}
TypeSpec::External(ExternalTypeSpec::Proto(PlannedProtoType::Enum(enumeration))) => {
enumeration.name.clone()
}
TypeSpec::External(ExternalTypeSpec::Json(json_type)) => json_type.model_name.clone(),
TypeSpec::Record(record) => record.model_name.clone(),
TypeSpec::Enum(enumeration) => enumeration.name.clone(),
TypeSpec::Flags(flags) => flags.name.clone(),
TypeSpec::Variant(variant) => variant.name.clone(),
TypeSpec::Resource(resource) => resource.type_name.clone(),
}
}
fn go_generic_declaration_name(
model_name: &str,
parameters: &[crate::spec::TypeParameterUsage],
) -> String {
if parameters.is_empty() {
return model_name.to_string();
}
let declarations = parameters
.iter()
.map(|usage| format!("{} any", usage.parameter.name))
.collect::<Vec<_>>()
.join(", ");
format!("{model_name}[{declarations}]")
}
fn render_go_model_type_parameter_declaration(
output: &mut String,
parameters: &[crate::spec::TypeParameterUsage],
) {
if parameters.is_empty() {
return;
}
output.push('[');
for (index, usage) in parameters.iter().enumerate() {
if index > 0 {
output.push_str(", ");
}
output.push_str(&usage.parameter.name);
output.push_str(" any");
}
output.push(']');
}
fn go_generic_reference(
model_name: &str,
parameters: &[crate::spec::TypeParameterUsage],
erase: bool,
) -> String {
if parameters.is_empty() {
return model_name.to_string();
}
let arguments = parameters
.iter()
.map(|usage| {
if erase {
"any"
} else {
usage.parameter.name.as_str()
}
})
.collect::<Vec<_>>()
.join(", ");
format!("{model_name}[{arguments}]")
}
fn type_needs_pointer_when_optional(value: &PlannedValueType, type_expr: &str) -> bool {
if matches!(
value,
PlannedValueType::Variant(_) | PlannedValueType::Unknown
) {
return false;
}
if matches!(
type_expr,
"client.VersioningOverride" | "temporal.SearchAttributes"
) {
return false;
}
if go_type_is_nilable(type_expr) {
return false;
}
true
}
pub(in crate::generator) fn go_type_is_nilable(type_expr: &str) -> bool {
matches!(type_expr, "any" | "error")
|| type_expr.starts_with("[]")
|| type_expr.starts_with("map[")
}
const GENERIC_MODEL_KEY_PREFIX: &str = "nexgen::generic::";
fn ensure_generic_tuple(
arity: usize,
models: &mut IndexMap<String, RenderedModel>,
) -> Result<String> {
if arity == 0 || arity > TUPLE_FIELD_NAMES.len() {
return Err(Error::UnsupportedGoType {
context: format!("tuple with {arity} element(s)"),
reason: format!(
"tuples are supported with 1 to {} elements",
TUPLE_FIELD_NAMES.len()
),
});
}
let type_name = format!("Tuple{arity}");
let key = format!("{GENERIC_MODEL_KEY_PREFIX}{type_name}");
if !models.contains_key(&key) {
let params = (1..=arity)
.map(|index| format!("T{index}"))
.collect::<Vec<_>>()
.join(", ");
let fields = (0..arity)
.map(|index| RenderedField {
name: TUPLE_FIELD_NAMES[index].to_string(),
doc: None,
go_type: format!("T{}", index + 1),
required: true,
})
.collect();
models.insert(
key,
RenderedModel {
name: format!("{type_name}[{params} any]"),
fields,
},
);
}
Ok(type_name)
}
fn ensure_generic_result(models: &mut IndexMap<String, RenderedModel>) {
let key = format!("{GENERIC_MODEL_KEY_PREFIX}Result");
if !models.contains_key(&key) {
models.insert(
key,
RenderedModel {
name: "Result[T, E any]".to_string(),
fields: vec![
RenderedField {
name: "Result".to_string(),
doc: None,
go_type: "T".to_string(),
required: false,
},
RenderedField {
name: "Error".to_string(),
doc: None,
go_type: "E".to_string(),
required: false,
},
],
},
);
}
}
pub(in crate::generator) fn go_unexported_name(name: &str) -> String {
let mut chars = name.chars();
match chars.next() {
None => String::new(),
Some(first) => {
let mut result = first.to_lowercase().to_string();
result.push_str(chars.as_str());
go_ident(&result)
}
}
}
pub(in crate::generator) fn go_field_name(name: &str) -> String {
let camel = name
.to_snake_case()
.split('_')
.filter(|word| !word.is_empty())
.map(|word| {
if word.eq_ignore_ascii_case("id") {
"ID".to_string()
} else {
word.to_upper_camel_case()
}
})
.collect::<String>();
go_ident(&camel)
}
fn go_ident(name: &str) -> String {
if is_go_keyword(name) {
format!("{name}_")
} else {
name.to_string()
}
}
fn parse_go_import(type_expr: &str) -> (Option<String>, String) {
if let Some(colon) = type_expr.find(':') {
let import_path = &type_expr[..colon];
let code_expr = &type_expr[colon + 1..];
return (Some(import_path.to_string()), code_expr.to_string());
}
if let Some(last_slash) = type_expr.rfind('/') {
if let Some(dot_offset) = type_expr[last_slash..].find('.') {
let split = last_slash + dot_offset;
let import_path = &type_expr[..split];
let pkg_name = &type_expr[last_slash + 1..split];
let type_name = &type_expr[split + 1..];
return (
Some(import_path.to_string()),
format!("{pkg_name}.{type_name}"),
);
}
}
if let Some(dot) = type_expr.find('.') {
let prefix = &type_expr[..dot];
if !prefix.is_empty()
&& prefix.chars().all(|c| c.is_ascii_lowercase() || c == '_')
&& type_expr[dot + 1..]
.chars()
.next()
.is_some_and(|c| c.is_ascii_uppercase())
{
return (Some(prefix.to_string()), type_expr.to_string());
}
}
(None, type_expr.to_string())
}
fn collect_imports_from_plan(api_plan: &PlannedSpec, imports: &mut BTreeSet<String>) {
for (_, model) in api_plan.records() {
for (_, field) in model.model_fields() {
collect_imports_from_value_type(
&planned_field_kind(&field.field_type, api_plan),
imports,
);
if field.required
&& let Some(function) = &field.function
{
collect_imports_from_function(function, imports);
}
}
}
for service in &api_plan.services {
for operation in &service.operations {
if let Some(input) = operation
.input
.as_ref()
.and_then(|input| planned_message_type(input, api_plan))
{
collect_imports_from_message_type(&input, imports);
}
if let PlannedOperationOutput::Message(output) = operation_output(operation, api_plan) {
collect_imports_from_message_type(&output, imports);
}
if let Some(transform) = &operation.output_transform {
if let (Some(annotation), Some(_expr)) = (
transform.type_name.for_language(Language::Go),
transform.transform.for_language(Language::Go),
) {
let (import_path, _) = parse_go_import(annotation);
if let Some(path) = import_path {
imports.insert(path);
}
}
}
}
}
}
fn collect_imports_from_function(function: &FunctionFieldSpec, imports: &mut BTreeSet<String>) {
match &function.args {
FunctionArgsSpec::Varargs { prefix, .. } => {
for arg in prefix {
collect_imports_from_authored_type(&arg.field_type, imports);
}
}
FunctionArgsSpec::Fixed(args) => {
for arg in args {
collect_imports_from_authored_type(&arg.field_type, imports);
}
}
}
match &function.result {
FunctionResultSpec::Authored(result) => collect_imports_from_authored_type(result, imports),
FunctionResultSpec::Annotation(result) => {
if let Some(annotation) = result.for_language(Language::Go) {
let (import_path, _) = parse_go_import(annotation);
if let Some(path) = import_path {
imports.insert(path);
}
}
}
}
}
fn collect_imports_from_authored_type(wit_type: &PlannedType, imports: &mut BTreeSet<String>) {
match wit_type {
TypeSpec::Option(inner) | TypeSpec::List(inner) => {
collect_imports_from_authored_type(inner, imports);
}
TypeSpec::Tuple(items) => {
for item in items {
collect_imports_from_authored_type(item, imports);
}
}
TypeSpec::Map(key, value) => {
collect_imports_from_authored_type(key, imports);
collect_imports_from_authored_type(value, imports);
}
TypeSpec::Result { ok, err } => {
if let Some(ok) = ok.as_deref() {
collect_imports_from_authored_type(ok, imports);
}
if let Some(err) = err.as_deref() {
collect_imports_from_authored_type(err, imports);
}
}
TypeSpec::External(ExternalTypeSpec::Alias(AliasTypeSpec {
type_name, target, ..
})) => {
if let Some(annotation) = type_name.for_language(Language::Go) {
let (import_path, _) = parse_go_import(annotation);
if let Some(path) = import_path {
imports.insert(path);
}
} else {
collect_imports_from_authored_type(target, imports);
}
}
_ => {}
}
}
fn collect_imports_from_message_type(message: &PlannedMessageType, imports: &mut BTreeSet<String>) {
if let Some(replacement) = &message.replacement {
if let Some(annotation) = replacement.type_name.for_language(Language::Go) {
let (import_path, _) = parse_go_import(annotation);
if let Some(path) = import_path {
imports.insert(path);
}
}
}
}
fn collect_imports_from_value_type(kind: &PlannedFieldKind, imports: &mut BTreeSet<String>) {
match kind {
PlannedFieldKind::Singular(value) | PlannedFieldKind::Repeated(value) => {
collect_imports_from_planned_value(value, imports);
}
PlannedFieldKind::Map { key, value } => {
collect_imports_from_planned_value(key, imports);
collect_imports_from_planned_value(value, imports);
}
}
}
fn collect_imports_from_planned_value(value: &PlannedValueType, imports: &mut BTreeSet<String>) {
match value {
PlannedValueType::TypeParameter(_) => {}
PlannedValueType::External {
type_name,
fallback,
} => {
if let Some(annotation) = type_name.for_language(Language::Go) {
let (import_path, _) = parse_go_import(annotation);
if let Some(path) = import_path {
imports.insert(path);
}
} else {
collect_imports_from_planned_value(fallback, imports);
}
}
PlannedValueType::Enum(enum_type) => {
if let Some(replacement) = &enum_type.replacement {
if let Some(annotation) = replacement.type_name.for_language(Language::Go) {
let (import_path, _) = parse_go_import(annotation);
if let Some(path) = import_path {
imports.insert(path);
}
}
}
}
PlannedValueType::Message(message_type) => {
if let Some(replacement) = &message_type.replacement {
if let Some(annotation) = replacement.type_name.for_language(Language::Go) {
let (import_path, _) = parse_go_import(annotation);
if let Some(path) = import_path {
imports.insert(path);
}
}
}
}
PlannedValueType::Tuple(items) => {
for item in items {
collect_imports_from_planned_value(item, imports);
}
}
PlannedValueType::Result { ok, err } => {
if let Some(ok) = ok {
collect_imports_from_planned_value(ok, imports);
}
if let Some(err) = err {
collect_imports_from_planned_value(err, imports);
}
}
_ => {}
}
}
fn is_go_keyword(name: &str) -> bool {
matches!(
name,
"break"
| "case"
| "chan"
| "const"
| "continue"
| "default"
| "defer"
| "else"
| "fallthrough"
| "for"
| "func"
| "go"
| "goto"
| "if"
| "import"
| "interface"
| "map"
| "package"
| "range"
| "return"
| "select"
| "struct"
| "switch"
| "type"
| "var"
)
}
fn render_file(
package: &GoPackageContext,
imports: &BTreeSet<String>,
external_imports: &[(String, String)],
external_model_body: &str,
operation_references: Option<&str>,
enums: &[&RenderedEnum],
flags: &[&RenderedFlags],
variants: &[&RenderedVariant],
models: &[(&String, &RenderedModel)],
services: &[RenderedService<'_>],
external_models: &GoExternalModels,
api_plan: &PlannedSpec,
visibility: &GoVisibility,
) -> String {
let mut output = String::new();
output.push_str(GENERATED_HEADER);
output.push_str("\n\n");
output.push_str("package ");
output.push_str(&package.package_name);
output.push('\n');
if !imports.is_empty() || !external_imports.is_empty() {
let stdlib: Vec<_> = imports.iter().filter(|p| !p.contains('.')).collect();
let third_party: Vec<_> = imports
.iter()
.filter(|p| p.contains('.'))
.filter(|p| {
!external_imports
.iter()
.any(|(external_path, _)| external_path == *p)
})
.collect();
output.push_str("\nimport (\n");
for import_path in &stdlib {
output.push('\t');
output.push('"');
output.push_str(import_path);
output.push('"');
output.push('\n');
}
if !stdlib.is_empty() && (!third_party.is_empty() || !external_imports.is_empty()) {
output.push('\n');
}
for import_path in &third_party {
output.push('\t');
output.push('"');
output.push_str(import_path);
output.push('"');
output.push('\n');
}
for (path, alias) in external_imports {
output.push('\t');
let default_alias = path.rsplit('/').next().unwrap_or(path);
if alias != default_alias {
output.push_str(alias);
output.push(' ');
}
output.push('"');
output.push_str(path);
output.push('"');
output.push('\n');
}
output.push_str(")\n");
}
if let Some(operation_references) = operation_references {
if !operation_references.is_empty() {
output.push('\n');
output.push_str(operation_references);
}
if !external_model_body.is_empty() {
output.push_str(external_model_body);
}
return output;
}
if !external_model_body.is_empty()
&& services.is_empty()
&& enums.is_empty()
&& flags.is_empty()
&& variants.is_empty()
&& models.is_empty()
{
output.push('\n');
output.push_str(external_model_body);
return output;
}
let has_operations = services.iter().any(|s| !s.operations.is_empty());
let private_enums = enums
.iter()
.copied()
.filter(|enumeration| !is_go_exported_decl(&enumeration.name))
.collect::<Vec<_>>();
let private_flags = flags
.iter()
.copied()
.filter(|flag_set| !is_go_exported_decl(&flag_set.name))
.collect::<Vec<_>>();
let private_variants = variants
.iter()
.copied()
.filter(|variant| !is_go_exported_decl(&variant.name))
.collect::<Vec<_>>();
let private_models = models
.iter()
.copied()
.filter(|(_, model)| !is_go_exported_type_decl(&model.name))
.collect::<Vec<_>>();
let has_private_datatypes = !private_enums.is_empty()
|| !private_flags.is_empty()
|| !private_variants.is_empty()
|| !private_models.is_empty();
if has_private_datatypes {
output.push_str("\n// --- Datatypes ---\n");
for enumeration in private_enums {
output.push('\n');
render_enum(&mut output, enumeration);
}
for flag_set in private_flags {
output.push('\n');
render_flags(&mut output, flag_set);
}
for variant in private_variants {
output.push('\n');
render_variant(&mut output, variant);
}
for (key, model) in private_models {
output.push('\n');
render_model(&mut output, key, model, external_models, false);
}
}
let has_resources = services.iter().any(|s| !s.resources.is_empty());
if has_resources {
output.push_str("\n// --- Resources ---\n");
for service in services {
for resource in &service.resources {
output.push('\n');
let constructor_returns_value = service.operations.iter().any(|operation| {
operation
.wire_binding
.as_ref()
.and_then(|binding| binding.returned_resource_type_name())
.is_some_and(|returned| returned == resource.type_name)
});
render_resource(
&mut output,
resource,
api_plan,
package,
visibility,
constructor_returns_value,
);
render_resource_methods(
&mut output,
service,
resource,
api_plan,
package,
visibility,
);
}
}
}
if has_operations {
output.push_str("\n// --- Operations (internal) ---\n");
for service in services {
for operation in &service.operations {
output.push('\n');
render_operation_function(&mut output, service, operation, package);
}
}
}
let public_enums = enums
.iter()
.copied()
.filter(|enumeration| is_go_exported_decl(&enumeration.name))
.collect::<Vec<_>>();
let public_flags = flags
.iter()
.copied()
.filter(|flag_set| is_go_exported_decl(&flag_set.name))
.collect::<Vec<_>>();
let public_variants = variants
.iter()
.copied()
.filter(|variant| is_go_exported_decl(&variant.name))
.collect::<Vec<_>>();
let public_models = models
.iter()
.copied()
.filter(|(_, model)| is_go_exported_type_decl(&model.name))
.collect::<Vec<_>>();
let has_public_datatypes = !public_enums.is_empty()
|| !public_flags.is_empty()
|| !public_variants.is_empty()
|| !public_models.is_empty();
let has_endpoint_operations = services
.iter()
.any(|service| service.endpoint.is_some() && !service.operations.is_empty());
if has_endpoint_operations || has_public_datatypes {
output.push_str("\n// --- Operations (public API) ---\n");
for enumeration in public_enums {
output.push('\n');
render_enum(&mut output, enumeration);
}
for flag_set in public_flags {
output.push('\n');
render_flags(&mut output, flag_set);
}
for variant in public_variants {
output.push('\n');
render_variant(&mut output, variant);
}
for (key, model) in public_models {
output.push('\n');
render_model(&mut output, key, model, external_models, true);
}
for service in services {
if service.endpoint.is_none() {
continue;
}
for operation in &service.operations {
if let Some(params) = &operation.unpacked_input {
output.push('\n');
render_options_struct(&mut output, operation, params);
output.push('\n');
render_convenience_wrapper(
&mut output,
service,
operation,
params,
package,
visibility,
);
if unary_primary_varargs_function_param(params).is_some() {
output.push('\n');
render_typed_convenience_wrapper(
&mut output,
service,
operation,
params,
package,
visibility,
);
}
} else {
output.push('\n');
render_empty_options_struct(&mut output, operation);
output.push('\n');
render_forwarding_wrapper(&mut output, service, operation, package);
}
}
}
}
output
}
pub(in crate::generator) fn render_go_doc_comment(output: &mut String, indent: &str, text: &str) {
fn neutralize_directive(line: &str) -> String {
let trimmed = line.trim_start();
let leading = &line[..line.len() - trimmed.len()];
if trimmed == "+build" || trimmed.starts_with("+build ") {
let rest = &trimmed["+build".len()..];
format!("{leading}\\+build{rest}")
} else if let Some(rest) = trimmed.strip_prefix("go:") {
format!("{leading}go\\:{rest}")
} else {
line.to_string()
}
}
let max_width = GO_DOC_COMMENT_LINE_LENGTH.saturating_sub(indent.chars().count() + 3);
for line in text.trim().lines() {
let line = line.trim();
if line.is_empty() {
output.push_str(indent);
output.push_str("//\n");
continue;
}
let mut current = String::new();
for word in line.split_whitespace() {
let separator_width = usize::from(!current.is_empty());
if !current.is_empty()
&& current.chars().count() + separator_width + word.chars().count() > max_width
{
output.push_str(indent);
output.push_str("// ");
output.push_str(&neutralize_directive(¤t));
output.push('\n');
current.clear();
}
if !current.is_empty() {
current.push(' ');
}
current.push_str(word);
}
output.push_str(indent);
output.push_str("// ");
output.push_str(&neutralize_directive(¤t));
output.push('\n');
}
}
fn render_field_doc_comment(
output: &mut String,
indent: &str,
name: &str,
doc: Option<&str>,
required: bool,
) {
let status = if required { "Required." } else { "Optional." };
let doc = doc.map(str::trim).filter(|doc| !doc.is_empty());
if let Some(doc) = doc {
render_go_doc_comment(output, indent, &format!("{name} - {doc}"));
output.push_str(indent);
output.push_str("//\n");
render_go_doc_comment(output, indent, status);
} else {
render_go_doc_comment(output, indent, &format!("{name} - {status}"));
}
}
fn render_operation_doc_comment(
output: &mut String,
operation: &RenderedOperation<'_>,
input_docs: &[(String, String)],
) {
let doc = operation.doc.map(str::trim).filter(|doc| !doc.is_empty());
let return_doc = operation
.return_doc
.map(str::trim)
.filter(|doc| !doc.is_empty());
if let Some(doc) = doc {
render_go_doc_comment(output, "", doc);
}
if !input_docs.is_empty() {
if doc.is_some() {
output.push_str("//\n");
}
for (name, doc) in input_docs {
render_go_doc_comment(output, "", &format!("Input {name}: {doc}"));
}
}
if let Some(return_doc) = return_doc {
if doc.is_some() || !input_docs.is_empty() {
output.push_str("//\n");
}
render_go_doc_comment(output, "", &format!("Returns: {return_doc}"));
}
}
fn render_enum(output: &mut String, enumeration: &RenderedEnum) {
output.push_str("type ");
output.push_str(&enumeration.name);
output.push_str(" int32\n");
if enumeration.values.is_empty() {
return;
}
output.push_str("\nconst (\n");
for value in &enumeration.values {
output.push('\t');
output.push_str(&enumeration.name);
output.push_str(&value.name);
output.push(' ');
output.push_str(&enumeration.name);
output.push_str(" = ");
output.push_str(&value.number.to_string());
output.push('\n');
}
output.push_str(")\n");
}
fn render_flags(output: &mut String, flags: &RenderedFlags) {
output.push_str("type ");
output.push_str(&flags.name);
output.push_str(" int32\n");
if flags.flags.is_empty() {
return;
}
output.push_str("\nconst (\n");
for flag in &flags.flags {
output.push('\t');
output.push_str(&flags.name);
output.push_str(&flag.name);
output.push(' ');
output.push_str(&flags.name);
output.push_str(" = 1 << ");
output.push_str(&flag.bit.to_string());
output.push('\n');
}
output.push_str(")\n");
}
fn render_variant(output: &mut String, variant: &RenderedVariant) {
output.push_str("type ");
output.push_str(&go_generic_declaration_name(
&variant.name,
&variant.type_parameters,
));
output.push_str(" interface {\n");
output.push('\t');
output.push_str(&variant.marker_method);
output.push('(');
output.push_str(
&variant
.type_parameters
.iter()
.map(|usage| usage.parameter.name.as_str())
.collect::<Vec<_>>()
.join(", "),
);
output.push_str(")\n");
output.push_str("}\n");
for case in &variant.cases {
output.push('\n');
output.push_str("type ");
output.push_str(&go_generic_declaration_name(
&case.struct_name,
&variant.type_parameters,
));
if let Some(payload_type) = &case.payload_type {
output.push_str(" struct {\n");
output.push_str("\tValue ");
output.push_str(payload_type);
output.push('\n');
output.push_str("}\n");
} else {
output.push_str(" struct{}\n");
}
output.push('\n');
output.push_str("func (");
output.push_str(&go_generic_reference(
&case.struct_name,
&variant.type_parameters,
false,
));
output.push_str(") ");
output.push_str(&variant.marker_method);
output.push('(');
output.push_str(
&variant
.type_parameters
.iter()
.map(|usage| usage.parameter.name.as_str())
.collect::<Vec<_>>()
.join(", "),
);
output.push_str(") {}\n");
}
}
fn render_model(
output: &mut String,
key: &str,
model: &RenderedModel,
external_models: &GoExternalModels,
public: bool,
) {
output.push_str("type ");
output.push_str(&model.name);
output.push_str(" struct {\n");
if model.fields.is_empty() {
output.push_str("}\n");
} else {
for field in &model.fields {
if public {
render_field_doc_comment(
output,
"\t",
&field.name,
field.doc.as_deref(),
field.required,
);
}
output.push('\t');
output.push_str(&field.name);
output.push(' ');
output.push_str(&field.go_type);
output.push('\n');
}
output.push_str("}\n");
}
external_models.render_model_wire_methods(output, key, model);
}
fn render_resource(
output: &mut String,
resource: &PlannedResource,
api_plan: &PlannedSpec,
package: &GoPackageContext,
visibility: &GoVisibility,
constructor_returns_value: bool,
) {
output.push_str("type ");
output.push_str(&resource.type_name);
output.push_str(" struct {\n");
if resource.fields.is_empty() {
output.push_str("}\n");
} else {
for field in &resource.fields {
let field_name = go_field_name(&field.name);
let field_kind = planned_field_kind(&field.kind, api_plan);
let go_type = visibility.rewrite_go_expr(&resolve_resource_field_kind(
&field_kind,
field.optional,
package,
));
if field.optional {
render_field_doc_comment(output, "\t", &field_name, None, false);
} else {
render_field_doc_comment(output, "\t", &field_name, None, true);
}
output.push('\t');
output.push_str(&field_name);
output.push(' ');
output.push_str(&go_type);
output.push('\n');
}
output.push_str("}\n");
}
output.push_str("\nfunc New");
output.push_str(&resource.type_name);
output.push('(');
for (index, field) in resource.fields.iter().enumerate() {
if index > 0 {
output.push_str(", ");
}
let name = go_unexported_name(&go_field_name(&field.name));
let kind = planned_field_kind(&field.kind, api_plan);
let internal = resolve_resource_field_kind(&kind, field.optional, package);
let public = if field.optional
&& public_default_punning_zero_for_field(&kind, &internal).is_some()
{
internal.trim_start_matches('*').to_string()
} else {
internal
};
output.push_str(&name);
output.push(' ');
output.push_str(&visibility.rewrite_go_expr(&public));
}
output.push_str(") ");
if !constructor_returns_value {
output.push('*');
}
output.push_str(&resource.type_name);
output.push_str(" {\n");
for field in resource.fields.iter().filter(|field| field.optional) {
let name = go_unexported_name(&go_field_name(&field.name));
let kind = planned_field_kind(&field.kind, api_plan);
let internal = resolve_resource_field_kind(&kind, true, package);
if let Some(zero) = public_default_punning_zero_for_field(&kind, &internal) {
output.push_str("\tvar ");
output.push_str(&name);
output.push_str("Ptr ");
output.push_str(&visibility.rewrite_go_expr(&internal));
output.push_str("\n\tif ");
output.push_str(&name);
output.push_str(" != ");
output.push_str(&zero);
output.push_str(" {\n\t\t");
output.push_str(&name);
output.push_str("Ptr = &");
output.push_str(&name);
output.push_str("\n\t}\n");
}
}
output.push_str("\treturn ");
if !constructor_returns_value {
output.push('&');
}
output.push_str(&resource.type_name);
output.push_str("{\n");
for field in &resource.fields {
let name = go_unexported_name(&go_field_name(&field.name));
let kind = planned_field_kind(&field.kind, api_plan);
let internal = resolve_resource_field_kind(&kind, field.optional, package);
let value = if field.optional
&& public_default_punning_zero_for_field(&kind, &internal).is_some()
{
format!("{name}Ptr")
} else {
name
};
output.push_str("\t\t");
output.push_str(&go_field_name(&field.name));
output.push_str(": ");
output.push_str(&value);
output.push_str(",\n");
}
output.push_str("\t}\n}\n");
}
pub(in crate::generator) fn resolve_resource_field_kind(
kind: &PlannedFieldKind,
optional: bool,
package: &GoPackageContext,
) -> String {
match kind {
PlannedFieldKind::Map { key, value } => {
let key_type = resolve_resource_value_type(key, package);
let value_type = resolve_resource_value_type(value, package);
format!("map[{key_type}]{value_type}")
}
PlannedFieldKind::Repeated(value) => {
let element_type = resolve_resource_value_type(value, package);
format!("[]{element_type}")
}
PlannedFieldKind::Singular(value) => {
let (base_type, _is_struct) =
resolve_resource_value_type_with_struct_flag(value, package);
if optional && type_needs_pointer_when_optional(value, &base_type) {
format!("*{base_type}")
} else {
base_type
}
}
}
}
fn resolve_resource_value_type(value: &PlannedValueType, package: &GoPackageContext) -> String {
resolve_resource_value_type_with_struct_flag(value, package).0
}
fn resolve_resource_value_type_with_struct_flag(
value: &PlannedValueType,
package: &GoPackageContext,
) -> (String, bool) {
match value {
PlannedValueType::TypeParameter(name) => (name.clone(), false),
PlannedValueType::Scalar(PlannedScalarType::Float) => ("float64".to_string(), false),
PlannedValueType::Scalar(PlannedScalarType::Int32) => ("int32".to_string(), false),
PlannedValueType::Scalar(PlannedScalarType::Int64) => ("int64".to_string(), false),
PlannedValueType::Scalar(PlannedScalarType::Bool) => ("bool".to_string(), false),
PlannedValueType::Scalar(PlannedScalarType::String) => ("string".to_string(), false),
PlannedValueType::Scalar(PlannedScalarType::Bytes) => ("[]byte".to_string(), false),
PlannedValueType::Enum(enum_type) => {
if let Some(replacement) = &enum_type.replacement
&& let Some(type_name) = go_replacement_type_name(replacement, package)
{
return (type_name, false);
}
(
enum_type
.name
.clone()
.unwrap_or_else(|| "int32".to_string()),
false,
)
}
PlannedValueType::Flags(flags_type) => (flags_type.name.clone(), false),
PlannedValueType::Variant(variant_type) => (variant_type.name.clone(), false),
PlannedValueType::Message(message_type) => {
if let Some(replacement) = &message_type.replacement
&& let Some(type_name) = go_replacement_type_name(replacement, package)
{
return (type_name, false);
}
if let Some(authored_type) = &message_type.authored_type {
return (go_authored_type_annotation(authored_type), false);
}
(message_type.model_name.clone(), true)
}
PlannedValueType::Tuple(items) => {
let args = items
.iter()
.map(|value| resolve_resource_value_type(value, package))
.collect::<Vec<_>>()
.join(", ");
(format!("Tuple{}[{args}]", items.len()), true)
}
PlannedValueType::Result { ok, err } => {
let ok_type = ok
.as_deref()
.map(|value| resolve_resource_value_type(value, package))
.unwrap_or_else(|| "struct{}".to_string());
let err_type = err
.as_deref()
.map(|value| resolve_resource_value_type(value, package))
.unwrap_or_else(|| "error".to_string());
(format!("Result[{ok_type}, {err_type}]"), true)
}
PlannedValueType::External {
type_name,
fallback,
} => {
if let Some(annotation) = type_name.for_language(Language::Go) {
(package.go_type_expr(annotation), false)
} else {
resolve_resource_value_type_with_struct_flag(fallback, package)
}
}
PlannedValueType::Unknown => ("any".to_string(), false),
}
}
fn render_resource_methods(
output: &mut String,
service: &RenderedService<'_>,
resource: &PlannedResource,
api_plan: &PlannedSpec,
package: &GoPackageContext,
visibility: &GoVisibility,
) {
if service.endpoint.is_none() {
return;
}
for method in &resource.methods {
output.push('\n');
let method_name = go_field_name(&method.name);
match &method.binding {
PlannedResourceMethodBinding::Operation {
operation_name,
request_plan,
..
} => {
let operation = service
.operations
.iter()
.find(|op| op.name == operation_name)
.expect("bound resource operation should exist on the service");
let primary_varargs_function = method.params.iter().find(|param| {
param.function.as_ref().is_some_and(|function| {
function.primary
&& matches!(function.args, FunctionArgsSpec::Varargs { .. })
})
});
output.push_str("func (u *");
output.push_str(&resource.type_name);
output.push_str(") ");
output.push_str(&method_name);
output.push_str("(ctx ");
output.push_str(&package.workflow_context_type());
for param in &method.params {
if primary_varargs_function.is_some_and(|primary| primary.name == param.name) {
continue;
}
output.push_str(", ");
output.push_str(&go_unexported_name(&go_field_name(¶m.name)));
output.push(' ');
let param_kind = planned_field_kind(¶m.kind, api_plan);
let internal_type =
resolve_resource_field_kind(¶m_kind, param.optional, package);
let param_type = if param.optional
&& public_default_punning_zero_for_field(¶m_kind, &internal_type)
.is_some()
{
internal_type.trim_start_matches('*').to_string()
} else {
internal_type
};
output.push_str(&visibility.rewrite_go_expr(¶m_type));
}
if let Some(primary) = primary_varargs_function {
output.push_str(", ");
output.push_str(&go_unexported_name(&go_field_name(&primary.name)));
output.push_str(" any, args ...any");
}
output.push_str(") ");
render_operation_future_return_type(output, package);
output.push_str(" {\n");
let function_name_var = primary_varargs_function.map(|primary| {
let param_name = go_unexported_name(&go_field_name(&primary.name));
let name_var = format!("{param_name}Name");
let synthetic = RenderedUnpackedParam {
field_name: go_field_name(&primary.name),
doc: None,
param_name,
go_type: "string".to_string(),
public_go_type: "any".to_string(),
default_punning_zero: None,
function: primary.function.clone(),
function_args: None,
required: true,
embed_in_options: false,
};
render_function_name_inline_assignment(output, &synthetic);
name_var
});
for param in method.params.iter().filter(|param| param.optional) {
let kind = planned_field_kind(¶m.kind, api_plan);
let internal = resolve_resource_field_kind(&kind, true, package);
if let Some(zero) = public_default_punning_zero_for_field(&kind, &internal) {
let name = go_unexported_name(&go_field_name(¶m.name));
output.push_str("\tvar ");
output.push_str(&name);
output.push_str("Ptr ");
output.push_str(&visibility.rewrite_go_expr(&internal));
output.push_str("\n\tif ");
output.push_str(&name);
output.push_str(" != ");
output.push_str(&zero);
output.push_str(" {\n\t\t");
output.push_str(&name);
output.push_str("Ptr = &");
output.push_str(&name);
output.push_str("\n\t}\n");
}
}
let method_param_sources = method
.params
.iter()
.map(|param| {
let name = go_unexported_name(&go_field_name(¶m.name));
let value = if primary_varargs_function
.is_some_and(|primary| primary.name == param.name)
{
function_name_var.clone().unwrap()
} else if param.optional {
let kind = planned_field_kind(¶m.kind, api_plan);
let internal = resolve_resource_field_kind(&kind, true, package);
if public_default_punning_zero_for_field(&kind, &internal).is_some() {
format!("{name}Ptr")
} else {
name
}
} else {
name
};
(param.name.clone(), value)
})
.collect::<BTreeMap<_, _>>();
let mut request_expr = render_request_plan(
request_plan,
go_field_name,
|name, value| format!("{name}: {value}"),
|msg_name, fields| {
let type_name = record_for_model_key(api_plan, msg_name)
.map(|m| visibility.adjusted_model_name(record_message_key(m), &m.name))
.unwrap_or_else(|| message_model_name(msg_name));
if fields.is_empty() {
format!("{type_name}{{}}")
} else {
format!("{type_name}{{{}}}", fields.join(", "))
}
},
|name| format!("u.{}", go_field_name(name)),
|name| {
method_param_sources
.get(name)
.cloned()
.unwrap_or_else(|| go_unexported_name(&go_field_name(name)))
},
);
if let Some(primary) = primary_varargs_function
&& let Some(args_param) = operation.unpacked_input.as_ref().and_then(|params| {
params
.iter()
.find(|param| param.function_args.as_ref() == primary.function.as_ref())
})
&& let Some(prefix) = request_expr.strip_suffix('}')
{
request_expr = format!("{prefix}, {}: args}}", args_param.field_name);
}
output.push_str("\treturn ");
output.push_str(&operation.func_name);
output.push_str("(ctx, ");
output.push_str(&request_expr);
output.push_str(")\n");
output.push_str("}\n");
}
PlannedResourceMethodBinding::Stub => {
output.push_str("func (u *");
output.push_str(&resource.type_name);
output.push_str(") ");
output.push_str(&method_name);
output.push_str("(ctx ");
output.push_str(&package.workflow_context_type());
for param in &method.params {
output.push_str(", ");
output.push_str(&go_unexported_name(&go_field_name(¶m.name)));
output.push(' ');
let param_kind = planned_field_kind(¶m.kind, api_plan);
let param_type =
resolve_resource_field_kind(¶m_kind, param.optional, package);
output.push_str(&visibility.rewrite_go_expr(¶m_type));
}
output.push_str(") ");
render_operation_future_return_type(output, package);
output.push_str(" {\n");
output.push_str("\tpanic(\"");
output.push_str(&resource.type_name);
output.push('.');
output.push_str(&method_name);
output.push_str(" is not yet implemented\")\n");
output.push_str("}\n");
}
}
}
}
pub(in crate::generator) fn render_operation_future_return_type(
output: &mut String,
package: &GoPackageContext,
) {
output.push_str(&package.workflow_future_type());
}
fn render_future_value_set(output: &mut String, value_expr: &str, value_is_pointer: bool) {
if value_is_pointer {
output.push_str("\t\tif ");
output.push_str(value_expr);
output.push_str(" == nil {\n");
output.push_str("\t\t\tresultSettable.SetError(fmt.Errorf(\"nexgen decoded required operation result was nil\"))\n");
output.push_str("\t\t\treturn\n");
output.push_str("\t\t}\n");
output.push_str("\t\tresultSettable.Set(*");
output.push_str(value_expr);
output.push_str(", nil)\n");
} else {
output.push_str("\t\tresultSettable.Set(");
output.push_str(value_expr);
output.push_str(", nil)\n");
}
}
pub(in crate::generator) fn render_operation_future_adapter(
output: &mut String,
package: &GoPackageContext,
_result_type: &str,
value_is_pointer: bool,
get_body: impl FnOnce(&mut String),
) {
output.push_str("\tresult, resultSettable := ");
output.push_str(&package.new_future());
output.push_str("(ctx)\n");
output.push_str("\t");
output.push_str(&package.workflow_go());
output.push_str("(ctx, func(ctx ");
output.push_str(&package.workflow_context_type());
output.push_str(") {\n");
get_body(output);
render_future_value_set(output, "value", value_is_pointer);
output.push_str("\t})\n");
output.push_str("\treturn result\n");
}
fn new_nexus_client_expr(endpoint: &str, service_name: &str, package: &GoPackageContext) -> String {
format!(
"{}({}, {})",
package.new_nexus_client(),
go_string_literal(endpoint),
go_string_literal(service_name)
)
}
fn render_operation_function(
output: &mut String,
service: &RenderedService<'_>,
operation: &RenderedOperation<'_>,
package: &GoPackageContext,
) {
if let Some(binding) = &operation.wire_binding {
proto::render_operation_function_proto(output, service, operation, binding, package);
return;
}
let operation_name = go_string_literal(operation.wire_name);
output.push_str("func ");
output.push_str(&operation.func_name);
render_go_model_type_parameter_declaration(output, &operation.model_type_parameters);
output.push_str("(ctx ");
output.push_str(&package.workflow_context_type());
output.push_str(", request ");
output.push_str(&operation.input_type);
output.push_str(") ");
render_operation_future_return_type(output, package);
output.push_str(" {\n");
let endpoint = service
.endpoint
.as_deref()
.expect("operations require endpoint");
output.push_str("\tc := ");
output.push_str(&new_nexus_client_expr(endpoint, service.wire_name, package));
output.push_str("\n\tfut := c.ExecuteOperation(ctx, ");
output.push_str(&operation_name);
output.push_str(", request, ");
output.push_str(&package.nexus_operation_options());
output.push_str(")\n");
if let (Some(transform_expr), Some(transform_type)) = (
operation.output_transform_expr,
operation.output_transform_type.as_ref(),
) {
render_operation_future_adapter(output, package, transform_type, false, |output| {
if let Some(raw_result_type) = &operation.raw_output_type {
output.push_str("\t\tvar result ");
output.push_str(raw_result_type);
output.push('\n');
output.push_str("\t\tif err := fut.Get(ctx, &result); err != nil {\n");
} else {
output.push_str("\t\tif err := fut.Get(ctx, nil); err != nil {\n");
}
output.push_str("\t\t\tresultSettable.SetError(err)\n");
output.push_str("\t\t\treturn\n");
output.push_str("\t\t}\n");
output.push_str("\t\tvalue, err := ");
output.push_str(transform_expr);
output.push('\n');
output.push_str("\t\tif err != nil {\n");
output.push_str("\t\t\tresultSettable.SetError(err)\n");
output.push_str("\t\t\treturn\n");
output.push_str("\t\t}\n");
});
} else {
output.push_str("\treturn fut\n");
}
output.push_str("}\n");
}
fn render_options_struct(
output: &mut String,
operation: &RenderedOperation<'_>,
params: &[RenderedUnpackedParam],
) {
output.push_str("type ");
output.push_str(&go_field_name(operation.name));
output.push_str("Options");
render_go_model_type_parameter_declaration(output, &operation.model_type_parameters);
output.push_str(" struct {\n");
for param in params.iter().filter(|p| param_belongs_in_options(p)) {
render_field_doc_comment(
output,
"\t",
¶m.field_name,
param.doc.as_deref(),
param.required,
);
output.push('\t');
if param.embed_in_options {
output.push_str(param.public_go_type.trim_start_matches('*'));
} else {
output.push_str(¶m.field_name);
output.push(' ');
output.push_str(¶m.public_go_type);
}
output.push('\n');
}
output.push_str("}\n");
}
fn render_empty_options_struct(output: &mut String, operation: &RenderedOperation<'_>) {
output.push_str("type ");
output.push_str(&go_field_name(operation.name));
output.push_str("Options");
render_go_model_type_parameter_declaration(output, &operation.model_type_parameters);
output.push_str(" struct {\n}\n");
}
fn param_belongs_in_options(param: &RenderedUnpackedParam) -> bool {
param.function.is_none() && param.function_args.is_none()
}
fn param_is_positional(param: &RenderedUnpackedParam) -> bool {
!param_belongs_in_options(param)
}
fn function_type_parameter_name(param: &RenderedUnpackedParam) -> String {
format!("{}F", param.field_name)
}
fn required_function_type_parameters(
params: &[RenderedUnpackedParam],
package: &GoPackageContext,
visibility: &GoVisibility,
) -> Vec<(String, String)> {
params
.iter()
.filter_map(|param| {
let function = param.function.as_ref()?;
if is_go_signal_function(function)
|| function.alternate_type.is_none()
|| (function.primary
&& matches!(function.args, FunctionArgsSpec::Varargs { ref prefix, .. } if !prefix.is_empty()))
{
return None;
}
Some((
function_type_parameter_name(param),
go_function_constraint(function, params, package, visibility),
))
})
.collect()
}
fn go_function_constraint(
function: &FunctionFieldSpec,
params: &[RenderedUnpackedParam],
package: &GoPackageContext,
visibility: &GoVisibility,
) -> String {
let args = match &function.args {
FunctionArgsSpec::Fixed(args) => args
.iter()
.map(|arg| {
let arg_field = arg.name.to_snake_case();
params
.iter()
.find(|param| {
param
.field_name
.eq_ignore_ascii_case(&go_field_name(&arg_field))
})
.map(|param| param.go_type.clone())
.unwrap_or_else(|| {
go_authored_function_type_expr(&arg.field_type, package, visibility)
})
})
.collect::<Vec<_>>(),
FunctionArgsSpec::Varargs { prefix, .. } => {
let mut args = prefix
.iter()
.map(|arg| go_authored_function_type_expr(&arg.field_type, package, visibility))
.collect::<Vec<_>>();
let vararg_type = params
.iter()
.find(|param| {
param
.field_name
.eq_ignore_ascii_case(&go_field_name(&function.args_field))
})
.map(|param| go_variadic_element_type(¶m.go_type))
.unwrap_or_else(|| "any".to_string());
args.push(format!("...{vararg_type}"));
args
}
};
let result_type = match &function.result {
FunctionResultSpec::Authored(result) => {
go_authored_function_type_expr(result, package, visibility)
}
FunctionResultSpec::Annotation(result) => result
.for_language(Language::Go)
.map(|result| package.go_type_expr(result))
.map(|result| visibility.rewrite_go_expr(&result))
.unwrap_or_else(|| "any".to_string()),
};
let function_type = format!("func({}) {}", args.join(", "), result_type);
if let Some(alternate_type) = &function.alternate_type {
let alternate_type = go_authored_function_type_expr(alternate_type, package, visibility);
format!("interface{{ ~{alternate_type} | {function_type} }}")
} else {
format!("interface{{ ~{function_type} }}")
}
}
fn go_function_type_expr(
function: &FunctionFieldSpec,
params: &[RenderedUnpackedParam],
package: &GoPackageContext,
visibility: &GoVisibility,
) -> String {
let constraint = go_function_constraint(function, params, package, visibility);
constraint
.strip_prefix("interface{ ~")
.and_then(|value| value.strip_suffix(" }"))
.unwrap_or(&constraint)
.to_string()
}
fn is_go_signal_function(function: &FunctionFieldSpec) -> bool {
!function.primary
&& function.args_field == "signal_input"
&& matches!(function.alternate_type.as_ref(), Some(TypeSpec::String))
}
fn is_go_signal_param(param: &RenderedUnpackedParam) -> bool {
param.function.as_ref().is_some_and(is_go_signal_function)
}
fn is_go_signal_args_param(param: &RenderedUnpackedParam) -> bool {
param
.function_args
.as_ref()
.is_some_and(is_go_signal_function)
}
fn ordered_positional_params(params: &[RenderedUnpackedParam]) -> Vec<&RenderedUnpackedParam> {
let mut ordered = Vec::new();
for signal in params.iter().filter(|param| is_go_signal_param(param)) {
ordered.push(signal);
if let Some(function) = &signal.function
&& let Some(args) = params
.iter()
.find(|param| param.function_args.as_ref() == Some(function))
{
ordered.push(args);
}
}
for param in params.iter().filter(|param| param_is_positional(param)) {
if !ordered
.iter()
.any(|item| item.field_name == param.field_name)
{
ordered.push(param);
}
}
ordered
}
fn go_variadic_element_type(go_type: &str) -> String {
go_type
.strip_prefix("[]")
.map(str::to_string)
.unwrap_or_else(|| "any".to_string())
}
fn go_authored_function_type_expr(
wit_type: &PlannedType,
package: &GoPackageContext,
visibility: &GoVisibility,
) -> String {
let expr = match wit_type {
TypeSpec::Option(inner) => {
format!(
"*{}",
go_authored_function_type_expr(inner, package, visibility)
)
}
TypeSpec::List(inner) => {
format!(
"[]{}",
go_authored_function_type_expr(inner, package, visibility)
)
}
TypeSpec::Map(key, value) => format!(
"map[{}]{}",
go_authored_function_type_expr(key, package, visibility),
go_authored_function_type_expr(value, package, visibility)
),
TypeSpec::External(ExternalTypeSpec::Alias(AliasTypeSpec {
type_name, target, ..
})) => type_name
.for_language(Language::Go)
.map(|annotation| package.go_type_expr(annotation))
.unwrap_or_else(|| go_authored_function_type_expr(target, package, visibility)),
_ => go_authored_type_annotation(wit_type),
};
visibility.rewrite_go_expr(&expr)
}
fn primary_varargs_args_param(params: &[RenderedUnpackedParam]) -> Option<&RenderedUnpackedParam> {
let primary_function = params.iter().find_map(|param| {
let function = param.function.as_ref()?;
if function.primary && matches!(function.args, FunctionArgsSpec::Varargs { .. }) {
Some(function)
} else {
None
}
})?;
params.iter().find(|param| {
!param.required
&& param
.function_args
.as_ref()
.is_some_and(|function| function == primary_function)
})
}
fn unary_primary_varargs_function_param(
params: &[RenderedUnpackedParam],
) -> Option<&RenderedUnpackedParam> {
let function_param = params.iter().find(|param| {
param.function.as_ref().is_some_and(|function| {
function.primary && matches!(function.args, FunctionArgsSpec::Varargs { .. })
})
})?;
let function = function_param.function.as_ref()?;
let FunctionArgsSpec::Varargs { prefix, .. } = &function.args else {
return None;
};
if prefix.is_empty() {
return None;
}
let args = primary_varargs_args_param(params)?;
(go_variadic_element_type(&args.go_type) == "any").then_some(function_param)
}
fn render_operation_wrapper_return_type(output: &mut String, package: &GoPackageContext) {
render_operation_future_return_type(output, package);
}
fn render_function_name_inlining(output: &mut String, params: &[RenderedUnpackedParam]) {
for param in params {
if param
.function
.as_ref()
.is_some_and(|function| !is_go_signal_function(function))
{
let name = function_name_local_var_for_params(param, params);
render_function_name_inline_assignment_named(output, param, &name);
}
}
}
fn render_function_name_inline_assignment(output: &mut String, param: &RenderedUnpackedParam) {
let name_var = function_name_local_var(param);
render_function_name_inline_assignment_named(output, param, &name_var);
}
fn render_function_name_inline_assignment_named(
output: &mut String,
param: &RenderedUnpackedParam,
name_var: &str,
) {
let accepts_alternate = param
.function
.as_ref()
.is_some_and(|function| function.alternate_type.is_some());
output.push('\t');
output.push_str(&name_var);
output.push_str(" := \"\"\n");
output.push_str("\t{\n");
if accepts_alternate {
output.push_str("\t\tswitch rv := reflect.ValueOf(");
} else {
output.push_str("\t\trv := reflect.ValueOf(");
}
output.push_str(¶m.param_name);
if accepts_alternate {
output.push_str("); rv.Kind() {\n");
output.push_str("\t\tcase reflect.String:\n\t\t\t");
output.push_str(&name_var);
output.push_str(" = rv.String()\n\t\tcase reflect.Func:\n\t");
} else {
output.push_str(")\n");
}
output.push_str("\t\tfullName := runtime.FuncForPC(rv.Pointer()).Name()\n");
output.push_str("\t\telements := strings.Split(fullName, \".\")\n");
output.push_str("\t\tshortName := elements[len(elements)-1]\n");
output.push_str("\t\t");
if accepts_alternate {
output.push('\t');
}
output.push_str(&name_var);
output.push_str(" = strings.TrimSuffix(shortName, \"-fm\")\n");
if accepts_alternate {
output.push_str("\t\tdefault:\n\t\t\tpanic(\"nexgen function name requires string or function\")\n\t\t}\n");
}
output.push_str("\t}\n");
}
fn function_name_local_var(param: &RenderedUnpackedParam) -> String {
format!("{}Name", param.param_name)
}
fn function_name_local_var_for_params(
target: &RenderedUnpackedParam,
params: &[RenderedUnpackedParam],
) -> String {
let base = function_name_local_var(target);
let occupied = params
.iter()
.map(|param| param.param_name.as_str())
.chain(["ctx", "opts", "arg", "args"])
.collect::<BTreeSet<_>>();
if !occupied.contains(base.as_str()) {
return base;
}
let mut suffix = 2;
loop {
let candidate = format!("{base}{suffix}");
if !occupied.contains(candidate.as_str()) {
return candidate;
}
suffix += 1;
}
}
fn render_exact_function_name_assignment(output: &mut String, param: &RenderedUnpackedParam) {
let name_var = function_name_local_var(param);
output.push('\t');
output.push_str(&name_var);
output.push_str(" := \"\"\n\t{\n");
output.push_str("\t\trv := reflect.ValueOf(");
output.push_str(¶m.param_name);
output.push_str(")\n");
output.push_str("\t\tfullName := runtime.FuncForPC(rv.Pointer()).Name()\n");
output.push_str("\t\telements := strings.Split(fullName, \".\")\n");
output.push_str("\t\tshortName := elements[len(elements)-1]\n\t\t");
output.push_str(&name_var);
output.push_str(" = strings.TrimSuffix(shortName, \"-fm\")\n\t}\n");
}
fn wrapper_input_docs<'a>(
params: impl IntoIterator<Item = &'a RenderedUnpackedParam>,
) -> Vec<(String, String)> {
params
.into_iter()
.filter_map(|param| {
let doc = param.doc.as_deref()?.trim();
if doc.is_empty() {
None
} else {
let name = if is_go_signal_args_param(param) {
signal_args_public_param_name(param)
} else {
param.param_name.clone()
};
Some((name, doc.to_string()))
}
})
.collect()
}
fn render_convenience_wrapper(
output: &mut String,
_service: &RenderedService<'_>,
operation: &RenderedOperation<'_>,
params: &[RenderedUnpackedParam],
package: &GoPackageContext,
visibility: &GoVisibility,
) {
let exported_name = go_field_name(operation.name);
let mut type_params = operation
.model_type_parameters
.iter()
.map(|usage| (usage.parameter.name.clone(), "any".to_string()))
.collect::<Vec<_>>();
type_params.extend(required_function_type_parameters(
params, package, visibility,
));
let positional_params = ordered_positional_params(params);
let input_docs = wrapper_input_docs(positional_params.iter().copied());
render_operation_doc_comment(output, operation, &input_docs);
let mut signature_params: Vec<(String, String)> = vec![(
"opts".to_string(),
go_generic_reference(
&format!("{exported_name}Options"),
&operation.model_type_parameters,
false,
),
)];
signature_params.extend(positional_params.iter().copied().map(|p| {
let (name, ty) = if is_go_signal_param(p) {
(p.param_name.clone(), "string".to_string())
} else if is_go_signal_args_param(p) {
(signal_args_public_param_name(p), "any".to_string())
} else if let Some(function) = &p.function {
let ty = if unary_primary_varargs_function_param(params)
.is_some_and(|primary| primary.field_name == p.field_name)
{
"any".to_string()
} else if function.alternate_type.is_some() {
function_type_parameter_name(p)
} else {
go_function_type_expr(function, params, package, visibility)
};
(p.param_name.clone(), ty)
} else if let Some(function) = &p.function_args {
let ty = if matches!(function.args, FunctionArgsSpec::Varargs { .. }) {
format!("...{}", go_variadic_element_type(&p.public_go_type))
} else {
p.public_go_type.clone()
};
(p.param_name.clone(), ty)
} else {
(p.param_name.clone(), p.public_go_type.clone())
};
(name, ty)
}));
let multiline_signature = signature_params.len() + 1 > 3;
output.push_str("func ");
output.push_str(&exported_name);
if !type_params.is_empty() {
output.push('[');
for (index, (name, constraint)) in type_params.iter().enumerate() {
if index > 0 {
output.push_str(", ");
}
output.push_str(name);
output.push(' ');
output.push_str(constraint);
}
output.push(']');
}
if multiline_signature {
output.push_str("(\n\tctx ");
output.push_str(&package.workflow_context_type());
output.push_str(",\n");
for (name, ty) in &signature_params {
output.push('\t');
output.push_str(name);
output.push(' ');
output.push_str(ty);
output.push_str(",\n");
}
output.push(')');
} else {
output.push_str("(ctx ");
output.push_str(&package.workflow_context_type());
for (name, ty) in &signature_params {
output.push_str(", ");
output.push_str(name);
output.push(' ');
output.push_str(ty);
}
output.push(')');
}
output.push(' ');
render_operation_wrapper_return_type(output, package);
output.push_str(" {\n");
render_public_default_punning_locals(output, params);
render_function_name_inlining(output, params);
output.push_str("\treturn ");
output.push_str(&operation.func_name);
output.push_str("(ctx, ");
output.push_str(&operation.input_type);
output.push_str("{\n");
for param in params {
output.push_str("\t\t");
output.push_str(¶m.field_name);
output.push_str(": ");
if is_go_signal_param(param) {
output.push_str(¶m.param_name);
} else if is_go_signal_args_param(param) {
output.push_str(&format!(
"{}{{{}}}",
param.go_type,
signal_args_public_param_name(param)
));
} else if param.function.is_some() {
output.push_str(&function_name_local_var_for_params(param, params));
} else if param.function_args.is_some() {
output.push_str(¶m.param_name);
} else if param.default_punning_zero.is_some() {
output.push_str(¶m.param_name);
} else if param.embed_in_options {
output.push('&');
output.push_str("opts.");
output.push_str(¶m.field_name);
} else {
output.push_str("opts.");
output.push_str(¶m.field_name);
}
output.push_str(",\n");
}
output.push_str("\t})\n");
output.push_str("}\n");
}
fn signal_args_public_param_name(param: &RenderedUnpackedParam) -> String {
param
.param_name
.strip_suffix("Args")
.map(|prefix| format!("{prefix}Arg"))
.unwrap_or_else(|| param.param_name.clone())
}
fn render_public_default_punning_locals(output: &mut String, params: &[RenderedUnpackedParam]) {
for param in params
.iter()
.filter(|param| param_belongs_in_options(param))
{
if let Some(zero) = ¶m.default_punning_zero {
output.push_str("\tvar ");
output.push_str(¶m.param_name);
output.push(' ');
output.push_str(¶m.go_type);
output.push('\n');
output.push_str("\tif opts.");
output.push_str(¶m.field_name);
output.push_str(" != ");
output.push_str(zero);
output.push_str(" {\n\t\t");
output.push_str(¶m.param_name);
output.push_str(" = &opts.");
output.push_str(¶m.field_name);
output.push_str("\n\t}\n");
}
}
}
fn render_operation_params(
output: &mut String,
signature_params: &[(String, String)],
package: &GoPackageContext,
) {
let multiline_signature = signature_params.len() + 1 > 3;
if multiline_signature {
output.push_str("(\n\tctx ");
output.push_str(&package.workflow_context_type());
output.push_str(",\n");
for (name, ty) in signature_params {
output.push('\t');
output.push_str(name);
output.push(' ');
output.push_str(ty);
output.push_str(",\n");
}
output.push(')');
} else {
output.push_str("(ctx ");
output.push_str(&package.workflow_context_type());
for (name, ty) in signature_params {
output.push_str(", ");
output.push_str(name);
output.push(' ');
output.push_str(ty);
}
output.push(')');
}
}
fn render_typed_convenience_wrapper(
output: &mut String,
_service: &RenderedService<'_>,
operation: &RenderedOperation<'_>,
params: &[RenderedUnpackedParam],
package: &GoPackageContext,
visibility: &GoVisibility,
) {
let primary = unary_primary_varargs_function_param(params)
.expect("typed wrapper requires a unary-capable primary function");
let function = primary.function.as_ref().unwrap();
let exported_name = format!("{}Typed", go_field_name(operation.name));
let options_name = format!("{}Options", go_field_name(operation.name));
let arg_type = format!("{}Arg", primary.field_name);
let result_type = function
.result_type_parameter
.clone()
.unwrap_or_else(|| format!("{}Result", primary.field_name));
let mut signature = vec![("opts".to_string(), options_name)];
for param in ordered_positional_params(params) {
if param.field_name == primary.field_name {
let mut args = match &function.args {
FunctionArgsSpec::Varargs { prefix, .. } => prefix
.iter()
.map(|arg| go_authored_function_type_expr(&arg.field_type, package, visibility))
.collect::<Vec<_>>(),
FunctionArgsSpec::Fixed(_) => Vec::new(),
};
args.push(arg_type.clone());
signature.push((
param.param_name.clone(),
format!("func({}) {result_type}", args.join(", ")),
));
signature.push(("arg".to_string(), arg_type.clone()));
} else if param.function_args.as_ref() == Some(function) {
continue;
} else if is_go_signal_param(param) {
signature.push((param.param_name.clone(), "string".to_string()));
} else if is_go_signal_args_param(param) {
signature.push((signal_args_public_param_name(param), "any".to_string()));
}
}
render_operation_doc_comment(
output,
operation,
&wrapper_input_docs(ordered_positional_params(params)),
);
output.push_str("func ");
output.push_str(&exported_name);
output.push('[');
output.push_str(&arg_type);
output.push_str(" any, ");
output.push_str(&result_type);
output.push_str(" any]");
render_operation_params(output, &signature, package);
output.push(' ');
render_operation_wrapper_return_type(output, package);
output.push_str(" {\n");
render_public_default_punning_locals(output, params);
render_exact_function_name_assignment(output, primary);
output.push_str("\treturn ");
output.push_str(&operation.func_name);
output.push_str("(ctx, ");
output.push_str(&operation.input_type);
output.push_str("{\n");
for param in params {
output.push_str("\t\t");
output.push_str(¶m.field_name);
output.push_str(": ");
if param.field_name == primary.field_name {
output.push_str(&function_name_local_var(param));
} else if param.function_args.as_ref() == Some(function) {
output.push_str(&format!("{}{{arg}}", param.go_type));
} else if is_go_signal_param(param) {
output.push_str(¶m.param_name);
} else if is_go_signal_args_param(param) {
output.push_str(&format!(
"{}{{{}}}",
param.go_type,
signal_args_public_param_name(param)
));
} else if param.default_punning_zero.is_some() {
output.push_str(¶m.param_name);
} else if param.embed_in_options {
output.push_str("&opts.");
output.push_str(¶m.field_name);
} else {
output.push_str("opts.");
output.push_str(¶m.field_name);
}
output.push_str(",\n");
}
output.push_str("\t})\n}\n");
}
fn render_forwarding_wrapper(
output: &mut String,
_service: &RenderedService<'_>,
operation: &RenderedOperation<'_>,
package: &GoPackageContext,
) {
let exported_name = go_field_name(operation.name);
render_operation_doc_comment(output, operation, &[]);
output.push_str("func ");
output.push_str(&exported_name);
output.push_str("(ctx ");
output.push_str(&package.workflow_context_type());
output.push_str(", _ ");
output.push_str(&exported_name);
output.push_str("Options, request ");
output.push_str(&operation.input_type);
output.push_str(") ");
render_operation_wrapper_return_type(output, package);
output.push_str(" {\n");
output.push_str("\treturn ");
output.push_str(&operation.func_name);
output.push_str("(ctx, request)\n");
output.push_str("}\n");
}
#[cfg(test)]
mod tests {
#[test]
fn recognizes_builtin_nilable_types() {
for type_expr in ["any", "error", "[]byte", "map[string]any"] {
assert!(super::go_type_is_nilable(type_expr), "{type_expr}");
}
for type_expr in ["string", "time.Duration", "temporal.RetryPolicy"] {
assert!(!super::go_type_is_nilable(type_expr), "{type_expr}");
}
}
}