use std::collections::{BTreeMap, HashSet};
use std::path::PathBuf;
use heck::{ToLowerCamelCase, ToUpperCamelCase};
use crate::error::{Error, Result};
use crate::generator::json_schema::dotnet as dotnet_json;
use crate::generator::proto::dotnet as dotnet_proto;
use crate::generator::{ExternalModelBackend, GeneratedFiles, GenerationMode};
use crate::language::Language;
use crate::planning::{
PlannedFamily, PlannedOperationResourceFieldBinding, PlannedProtoType, PlannedResource,
PlannedResourceMethod, PlannedResourceMethodBindingSpec, PlannedResourceMethodResultKind,
PlannedSpec, PlannedType, operation_input_model,
};
use crate::planning::{RequestPlan, RequestPlanSource, ResolvedResourceBindingSource};
use crate::spec::{ApiSpecBranch, ApiSpecNode};
use crate::spec::{
EnumSpec, ExternalTypeSpec, FlagsSpec, FunctionFieldSpec, IntSpec, OperationSpec,
RecordFieldSpec, RecordSpec, ServiceSpec, SupportFragmentSpec, TypeSpec, VariantSpec,
};
const GENERATED_HEADER: &str = concat!(
"// <auto-generated />\n// Generated by nexgen v",
env!("CARGO_PKG_VERSION"),
". DO NOT EDIT!"
);
const GENERATED_CODE_ATTRIBUTE: &str = "[GeneratedCode(\"nexgen\", null)]";
const EXPERIMENTAL_WARNING: &str =
"WARNING: This API is experimental and may change in the future.";
type PlannedOperation = OperationSpec<PlannedFamily>;
type PlannedService = ServiceSpec<PlannedFamily>;
type PlannedModel = RecordSpec<PlannedFamily>;
type PlannedFlags = FlagsSpec;
type PlannedVariant = VariantSpec<PlannedFamily>;
trait PlannedOperationExt {
fn input_model(&self) -> &PlannedType;
}
impl PlannedOperationExt for PlannedOperation {
fn input_model(&self) -> &PlannedType {
operation_input_model(self).expect("dotnet operation input should be present")
}
}
#[derive(Debug)]
struct ApiPlanner<'a> {
api_plan: &'a PlannedSpec,
external_models: DotNetExternalModels,
external_model_fragments: DotNetExternalModelFragments,
support_namespace: Option<&'a str>,
}
impl<'a> ApiPlanner<'a> {
fn new(api_plan: &'a PlannedSpec, support_namespace: Option<&'a str>) -> Result<Self> {
let external_models = DotNetExternalModels::new(api_plan)?;
let external_model_fragments = external_models.render_models()?;
Ok(Self {
api_plan,
external_models,
external_model_fragments,
support_namespace,
})
}
fn render_models_file(&self, namespace: &str) -> String {
let models = self
.api_plan
.records()
.map(|(_, record)| record)
.collect::<Vec<_>>();
let needs_result = models.iter().any(|model| {
model
.model_fields()
.any(|(_, field)| field_kind_uses_result(&field.field_type))
});
let mut imports = vec![
"System",
"System.CodeDom.Compiler",
"System.Collections.Generic",
];
if models.iter().any(|model| {
self.external_models
.model_transfer_converter_attribute(model)
.is_some()
}) {
imports.push("Temporalio.Converters");
}
if self.external_model_fragments.has_models() {
imports.push("System.Text.Json");
imports.push("System.Text.Json.Serialization");
}
if models.iter().any(|model| {
self.external_models
.model_uses_support_extensions(model, self.api_plan)
}) && let Some(support_namespace) = self.support_namespace
{
imports.push(support_namespace);
}
let module_imports = dotnet_module_imports(self.api_plan);
imports.extend(module_imports.iter().map(String::as_str));
let mut output = generated_file_prelude(namespace, &imports);
if needs_result {
render_result_helper(&mut output);
}
for enumeration in self.api_plan.enums().map(|(_, enumeration)| enumeration) {
render_enum(&mut output, enumeration);
}
for flag_set in self.api_plan.flags().map(|(_, flag_set)| flag_set) {
render_flags(&mut output, flag_set);
}
for variant in self.api_plan.variants().map(|(_, variant)| variant) {
self.render_variant(&mut output, variant);
}
for model in models {
self.render_model(&mut output, model);
}
output.push_str(&self.external_model_fragments.body);
close_namespace(&mut output);
output
}
fn render_service_file(&self, namespace: &str, include_native_api: bool) -> String {
let module_imports = dotnet_module_imports(self.api_plan);
let has_endpoint_operations = self
.api_plan
.services
.iter()
.any(|service| service.endpoint.is_some() && !service.operations.is_empty());
let mut imports = vec![
"System",
"System.CodeDom.Compiler",
"System.Collections.Generic",
"System.Threading.Tasks",
"NexusRpc",
];
if has_endpoint_operations {
imports.push("Temporalio.Converters");
}
if include_native_api
&& self
.api_plan
.services
.iter()
.any(|service| service.endpoint.is_none() && !service.operations.is_empty())
{
imports.push("Temporalio.Workflows");
}
imports.extend(module_imports.iter().map(String::as_str));
let mut output = generated_file_prelude(namespace, &imports);
for service in &self.api_plan.services {
render_xml_summary(&mut output, "", None, service.experimental);
output.push_str(GENERATED_CODE_ATTRIBUTE);
output.push('\n');
output.push_str("[NexusService(");
output.push_str(&csharp_string_literal(&service.wire_name));
output.push_str(")]\n");
output.push_str("internal interface I");
output.push_str(&csharp_type_name(&service.name));
output.push_str("\n{\n");
for operation in &service.operations {
self.render_service_operation(&mut output, operation);
}
output.push_str("}\n\n");
if include_native_api && service.endpoint.is_none() {
for operation in &service.operations {
self.render_operation_options_type(&mut output, operation);
}
let service_type = format!("I{}", csharp_type_name(&service.name));
self.render_endpoint_service_class(&mut output, service, &service_type);
}
}
self.render_operation_registry(&mut output);
close_namespace(&mut output);
output
}
fn render_operations_file(&self, namespace: &str) -> String {
let mut imports = vec![
"System",
"System.CodeDom.Compiler",
"System.Collections.Generic",
"System.Linq",
"System.Linq.Expressions",
"System.Threading.Tasks",
"Google.Protobuf.WellKnownTypes",
"Temporalio.Workflows",
];
let module_imports = dotnet_module_imports(self.api_plan);
imports.extend(module_imports.iter().map(String::as_str));
let mut output = generated_file_prelude(namespace, &imports);
for service in &self.api_plan.services {
self.render_operations_class(&mut output, service);
}
close_namespace(&mut output);
output
}
fn render_resources_file(&self, namespace: &str) -> String {
let mut imports = vec![
"System",
"System.CodeDom.Compiler",
"System.Collections.Generic",
"System.Threading.Tasks",
"Temporalio.Workflows",
];
let module_imports = dotnet_module_imports(self.api_plan);
imports.extend(module_imports.iter().map(String::as_str));
let mut output = generated_file_prelude(namespace, &imports);
for service in &self.api_plan.services {
for resource in &service.resources {
self.render_resource(&mut output, service, &resource.data);
}
}
close_namespace(&mut output);
output
}
fn render_service_operation(&self, output: &mut String, operation: &PlannedOperation) {
render_operation_summary_xml_doc(output, " ", operation);
output.push_str(" ");
output.push_str(GENERATED_CODE_ATTRIBUTE);
output.push('\n');
output.push_str(" [NexusOperation(");
output.push_str(&csharp_string_literal(&operation.wire_name));
output.push_str(")]\n");
output.push_str(" ");
let service_return_type = match operation.output_type() {
Some(output_type @ PlannedType::Record(_)) => {
self.dotnet_erased_model_type(output_type)
}
_ => self.operation_raw_return_type(operation),
};
output.push_str(&service_return_type);
output.push(' ');
output.push_str(&csharp_type_name(&operation.name));
output.push('(');
if self.operation_has_input(operation) {
output.push_str(&self.dotnet_erased_model_type(operation.input_model()));
output.push_str(" request");
}
output.push_str(");\n\n");
}
fn render_operation_registry(&self, output: &mut String) {
let operations = self
.api_plan
.services
.iter()
.filter(|service| service.endpoint.is_some())
.flat_map(|service| {
service
.operations
.iter()
.map(move |operation| (service, operation))
})
.collect::<Vec<_>>();
if operations.is_empty() {
return;
}
output.push_str("internal interface INexgenOperationInfo\n{\n");
output.push_str(" OperationDefinition Operation { get; }\n\n");
output.push_str(" Func<object, ISerializationContext>? SerializationContext { get; }\n");
output.push_str("}\n\n");
output.push_str(
"internal sealed class NexgenOperationInfo<TRequest, TResponse> : INexgenOperationInfo\n{\n",
);
output.push_str(" internal NexgenOperationInfo(\n");
output.push_str(" OperationDefinition operation,\n");
output.push_str(
" Func<TRequest, ISerializationContext>? serializationContext = null)\n",
);
output.push_str(" {\n");
output.push_str(" Operation = operation;\n");
output.push_str(" SerializationContext = serializationContext;\n");
output.push_str(" }\n\n");
output.push_str(" internal OperationDefinition Operation { get; }\n\n");
output.push_str(" OperationDefinition INexgenOperationInfo.Operation => Operation;\n\n");
output.push_str(
" internal Func<TRequest, ISerializationContext>? SerializationContext { get; }\n",
);
output.push_str(
"\n Func<object, ISerializationContext>? INexgenOperationInfo.SerializationContext => SerializationContext == null ? null : request => SerializationContext((TRequest)request);\n",
);
output.push_str("}\n\n");
output.push_str("internal static class NexgenOperationRegistry\n{\n");
for service in self
.api_plan
.services
.iter()
.filter(|service| service.endpoint.is_some() && !service.operations.is_empty())
{
output.push_str(" private static readonly ServiceDefinition ");
output.push_str(&service_definition_field_name(service));
output.push_str(" = ServiceDefinition.FromType<I");
output.push_str(&csharp_type_name(&service.name));
output.push_str(">();\n");
}
output.push('\n');
output.push_str(" internal static IReadOnlyDictionary<(string Service, string Operation), INexgenOperationInfo> Operations { get; } = new Dictionary<(string Service, string Operation), INexgenOperationInfo>\n {\n");
for (service, operation) in operations {
output.push_str(" [(");
output.push_str(&csharp_string_literal(&service.wire_name));
output.push_str(", ");
output.push_str(&csharp_string_literal(&operation.wire_name));
output.push_str(")] = new NexgenOperationInfo<");
output.push_str(&self.operation_registry_request_type(operation));
output.push_str(", ");
output.push_str(&self.operation_registry_response_type(operation));
output.push_str(">(\n");
output.push_str(" ");
output.push_str(&service_definition_field_name(service));
output.push_str(".Operations[");
output.push_str(&csharp_string_literal(&operation.wire_name));
output.push_str("]");
if let Some(serialization_context) = operation
.serialization_context
.for_language(Language::Dotnet)
{
output.push_str(",\n serializationContext: ");
output.push_str(&qualify_dotnet_support_reference(
serialization_context,
self.support_namespace,
));
}
output.push_str("),\n");
}
output.push_str(" };\n");
output.push_str("}\n\n");
}
fn render_operations_class(&self, output: &mut String, service: &PlannedService) {
if service.endpoint.is_none() {
return;
}
let service_type = format!("I{}", csharp_type_name(&service.name));
for operation in &service.operations {
self.render_operation_options_type(output, operation);
}
let explicit_operations_class = service
.operations_class
.for_language(Language::Dotnet)
.is_some();
if !explicit_operations_class {
output.push_str(GENERATED_CODE_ATTRIBUTE);
output.push('\n');
}
output.push_str("public static ");
if explicit_operations_class {
output.push_str("partial ");
}
output.push_str("class ");
output.push_str(&dotnet_operations_class_name(service));
output.push_str("\n{\n");
if let Some(endpoint) = &service.endpoint {
output.push_str(" ");
output.push_str(GENERATED_CODE_ATTRIBUTE);
output.push('\n');
output.push_str(" private const string ");
output.push_str(&service_endpoint_constant_name(service));
output.push_str(" = ");
output.push_str(&csharp_string_literal(endpoint));
output.push_str(";\n\n");
}
for operation in &service.operations {
self.render_operation_extension(
output,
operation,
&service_type,
&service_endpoint_constant_name(service),
service.endpoint.is_none(),
);
}
output.push_str("}\n\n");
}
fn render_endpoint_service_class(
&self,
output: &mut String,
service: &PlannedService,
service_type: &str,
) {
let service_class_name = dotnet_endpoint_service_class_name(service);
output.push_str(GENERATED_CODE_ATTRIBUTE);
output.push('\n');
output.push_str("public class ");
output.push_str(&service_class_name);
output.push_str("\n{\n");
output.push_str(" private readonly NexusWorkflowClient<");
output.push_str(service_type);
output.push_str("> _client;\n\n");
output.push_str(" public ");
output.push_str(&service_class_name);
output.push_str("(string endpoint)\n {\n");
output.push_str(" _client = Workflow.CreateNexusWorkflowClient<");
output.push_str(service_type);
output.push_str(">(endpoint);\n");
output.push_str(" }\n\n");
for operation in &service.operations {
self.render_endpoint_service_operation(output, operation, service_type);
}
output.push_str("}\n\n");
}
fn render_resource(
&self,
output: &mut String,
service: &PlannedService,
resource: &PlannedResource,
) {
let type_name = csharp_type_name(&resource.type_name);
output.push_str(GENERATED_CODE_ATTRIBUTE);
output.push('\n');
output.push_str("public class ");
output.push_str(&type_name);
output.push_str("\n{\n");
output.push_str(" public ");
output.push_str(&type_name);
output.push('(');
for (index, field) in resource.fields.iter().enumerate() {
if index > 0 {
output.push_str(", ");
}
output.push_str(&self.resource_field_type(&field.kind, field.optional));
output.push(' ');
output.push_str(&csharp_parameter_name(&field.name));
}
output.push_str(")\n {\n");
for field in &resource.fields {
output.push_str(" ");
output.push_str(&csharp_type_name(&field.name));
output.push_str(" = ");
output.push_str(&csharp_parameter_name(&field.name));
output.push_str(";\n");
}
output.push_str(" }\n\n");
for field in &resource.fields {
output.push_str(" public ");
output.push_str(&self.resource_field_type(&field.kind, field.optional));
output.push(' ');
output.push_str(&csharp_type_name(&field.name));
output.push_str(" { get; }\n");
}
if service.endpoint.is_none() {
output.push_str("\n internal NexusWorkflowClient<I");
output.push_str(&csharp_type_name(&service.name));
output.push_str(">? NexgenClient { get; set; }\n");
output.push_str("\n internal static ");
output.push_str(&type_name);
output.push_str(" BindClient(");
output.push_str(&type_name);
output.push_str(" resource, NexusWorkflowClient<I");
output.push_str(&csharp_type_name(&service.name));
output.push_str("> client)\n {\n");
output.push_str(" resource.NexgenClient = client;\n");
output.push_str(" return resource;\n");
output.push_str(" }\n");
output.push_str("\n private NexusWorkflowClient<I");
output.push_str(&csharp_type_name(&service.name));
output.push_str("> RequireNexgenClient()\n {\n");
output
.push_str(" return NexgenClient ?? throw new InvalidOperationException(\"");
output.push_str(&type_name);
output.push_str(" methods require a service endpoint.\");\n");
output.push_str(" }\n");
}
if !resource.fields.is_empty() && !resource.methods.is_empty() {
output.push('\n');
}
for method in &resource.methods {
self.render_resource_method(output, service, method);
}
output.push_str("}\n\n");
}
fn render_resource_method(
&self,
output: &mut String,
service: &PlannedService,
method: &PlannedResourceMethod,
) {
let operation = match &method.binding {
PlannedResourceMethodBindingSpec::Operation { operation_name, .. } => service
.operations
.iter()
.find(|operation| operation.name == *operation_name),
PlannedResourceMethodBindingSpec::Stub => None,
};
let return_type = self.resource_method_task_return_type(method);
output.push_str(" ");
output.push_str(GENERATED_CODE_ATTRIBUTE);
output.push('\n');
output.push_str(" public ");
if service.endpoint.is_none()
&& matches!(
method.binding,
PlannedResourceMethodBindingSpec::Operation { .. }
)
{
output.push_str("async ");
}
output.push_str(&return_type);
output.push(' ');
output.push_str(&csharp_type_name(&method.name));
output.push_str("Async");
output.push('(');
for (index, param) in method.params.iter().enumerate() {
if index > 0 {
output.push_str(", ");
}
output.push_str(&self.resource_field_type(¶m.kind, param.optional));
output.push(' ');
output.push_str(&csharp_parameter_name(¶m.name));
}
output.push(')');
match &method.binding {
PlannedResourceMethodBindingSpec::Operation {
operation_name: _,
request_plan,
..
} => {
let operation =
operation.expect("bound resource operation should exist on the service");
output.push_str("\n {\n");
if service.endpoint.is_none() {
self.render_resource_method_inline_operation_body(
output,
operation,
request_plan,
);
} else {
render_resource_method_operation_body(
output,
service,
operation,
request_plan,
self.api_plan,
None,
);
}
output.push_str(" }\n\n");
}
PlannedResourceMethodBindingSpec::Stub => {
output.push_str(" => throw new NotSupportedException(\"Resource method is not bound to a Nexus operation.\");\n\n");
}
}
}
fn render_variant(&self, output: &mut String, variant: &PlannedVariant) {
let type_parameters = self
.api_plan
.variant_type_parameters(&variant.full_name, Language::Dotnet);
let type_arguments = type_parameters
.iter()
.map(|usage| usage.parameter.name.as_str())
.collect::<Vec<_>>()
.join(", ");
output.push_str(GENERATED_CODE_ATTRIBUTE);
output.push('\n');
output.push_str("public abstract record ");
output.push_str(&csharp_type_name(&variant.name));
if !type_parameters.is_empty() {
output.push('<');
output.push_str(&type_arguments);
output.push('>');
}
output.push_str("\n{\n");
output.push_str(" private ");
output.push_str(&csharp_type_name(&variant.name));
output.push_str("() { }\n\n");
for (index, case) in variant.cases.iter().enumerate() {
let case_name = csharp_type_name(&case.name);
output.push_str(" ");
output.push_str(GENERATED_CODE_ATTRIBUTE);
output.push('\n');
output.push_str(" public sealed record ");
output.push_str(&case_name);
if let Some(payload) = &case.payload {
let payload_type = self.dotnet_value_type(payload);
output.push('(');
output.push_str(&payload_type);
output.push_str(" Value) : ");
} else {
output.push_str(" : ");
}
output.push_str(&csharp_type_name(&variant.name));
if !type_parameters.is_empty() {
output.push('<');
output.push_str(&type_arguments);
output.push('>');
}
output.push_str(";\n");
if index + 1 < variant.cases.len() {
output.push('\n');
}
}
output.push_str("}\n\n");
}
fn render_model(&self, output: &mut String, model: &PlannedModel) {
render_xml_summary(output, "", dotnet_doc(model.doc()), model.experimental);
if let Some(attribute) = self
.external_models
.model_transfer_converter_attribute(model)
{
output.push_str(&attribute);
output.push('\n');
}
output.push_str(GENERATED_CODE_ATTRIBUTE);
output.push('\n');
let access = self.model_access(model);
output.push_str(access);
output.push_str(" class ");
let type_name = csharp_type_name(&model.name);
output.push_str(&type_name);
let type_parameters = self
.api_plan
.record_type_parameters(&model.full_name, Language::Dotnet);
if !type_parameters.is_empty() {
output.push('<');
output.push_str(
&type_parameters
.iter()
.map(|usage| usage.parameter.name.as_str())
.collect::<Vec<_>>()
.join(", "),
);
output.push('>');
}
output.push_str("\n{\n");
self.render_model_constructor(output, access, &type_name, model);
for (field_name, field) in model.model_fields() {
render_field_xml_doc(output, " ", field);
output.push_str(" public ");
output.push_str(&self.model_field_type(model, field_name, field));
output.push(' ');
output.push_str(&field_property_name(field));
if field.required {
output.push_str(" { get; }\n");
} else {
output.push_str(" { get; init; }\n");
}
}
for (_field_name, field, source_expr) in model.sourced_fields() {
render_field_xml_doc(output, " ", field);
let property_name = field_property_name(field);
let backing_field_name = format!(
"_{}",
csharp_parameter_name(&field.name).trim_start_matches('@')
);
output.push_str(" private ");
output.push_str(&nullable_type(&self.field_type(field)));
output.push(' ');
output.push_str(&backing_field_name);
output.push_str(";\n");
output.push_str(" public ");
output.push_str(&self.field_type(field));
output.push(' ');
output.push_str(&property_name);
output.push_str("\n {\n");
output.push_str(" get => ");
output.push_str(&backing_field_name);
output.push_str(" ?? ");
output.push_str(&qualify_dotnet_support_call(
source_expr,
self.support_namespace,
));
output.push_str(";\n");
output.push_str(" init => ");
output.push_str(&backing_field_name);
output.push_str(" = value;\n");
output.push_str(" }\n");
}
if self.external_models.model_needs_wire_method(model) {
if model.public_fields().next().is_some() || model.sourced_fields().next().is_some() {
output.push('\n');
}
self.external_models.render_model_wire_methods(
output,
model,
self.api_plan,
self.support_namespace,
);
}
output.push_str("}\n\n");
}
fn render_operation_extension(
&self,
output: &mut String,
operation: &PlannedOperation,
service_type: &str,
endpoint_constant_name: &str,
endpoint_parameter: bool,
) {
let method_name = format!("{}Async", csharp_type_name(&operation.name));
let raw_return = self.operation_raw_return_type(operation);
let high_level_return = self.operation_return_type(operation);
let has_input = self.operation_has_input(operation);
let raw_input_type = has_input
.then(|| self.operation_raw_input_type(operation.input_model()))
.unwrap_or_default();
let high_level_input_type = has_input
.then(|| self.operation_input_type(operation.input_model()))
.unwrap_or_default();
if high_level_input_type == raw_input_type {
self.render_operation_request_extension(
output,
operation,
service_type,
&method_name,
has_input,
&raw_input_type,
&raw_return,
&high_level_return,
RequestArgumentKind::Raw,
endpoint_constant_name,
endpoint_parameter,
);
}
if has_input && high_level_input_type != raw_input_type {
self.render_operation_request_extension(
output,
operation,
service_type,
&method_name,
has_input,
&high_level_input_type,
&raw_return,
&high_level_return,
RequestArgumentKind::HighLevel,
endpoint_constant_name,
endpoint_parameter,
);
}
if has_input
&& let PlannedType::Record(input_record) = operation.input_model()
&& let Some(model) = self.api_plan.record(&input_record.full_name)
&& operation_has_flattened_convenience(operation, model, self.api_plan)
{
self.render_operation_flattened_extension(
output,
operation,
&method_name,
model,
&raw_return,
&high_level_return,
endpoint_parameter,
false,
None,
endpoint_parameter.then_some("endpoint"),
);
}
}
fn render_endpoint_service_operation(
&self,
output: &mut String,
operation: &PlannedOperation,
service_type: &str,
) {
let method_name = format!("{}Async", csharp_type_name(&operation.name));
let raw_return = self.operation_raw_return_type(operation);
let high_level_return = self.operation_return_type(operation);
let has_input = self.operation_has_input(operation);
let raw_input_type = has_input
.then(|| self.operation_raw_input_type(operation.input_model()))
.unwrap_or_default();
let high_level_input_type = has_input
.then(|| self.operation_input_type(operation.input_model()))
.unwrap_or_default();
if high_level_input_type == raw_input_type {
self.render_endpoint_service_request_method(
output,
operation,
service_type,
&method_name,
has_input,
&raw_input_type,
&raw_return,
&high_level_return,
RequestArgumentKind::Raw,
);
}
if has_input && high_level_input_type != raw_input_type {
self.render_endpoint_service_request_method(
output,
operation,
service_type,
&method_name,
has_input,
&high_level_input_type,
&raw_return,
&high_level_return,
RequestArgumentKind::HighLevel,
);
}
if has_input
&& let PlannedType::Record(input_record) = operation.input_model()
&& let Some(model) = self.api_plan.record(&input_record.full_name)
&& operation_has_flattened_convenience(operation, model, self.api_plan)
{
self.render_operation_flattened_extension(
output,
operation,
&method_name,
model,
&raw_return,
&high_level_return,
false,
true,
None,
None,
);
}
}
fn render_endpoint_service_request_method(
&self,
output: &mut String,
operation: &PlannedOperation,
_service_type: &str,
method_name: &str,
has_input: bool,
input_type: &str,
raw_return: &str,
high_level_return: &str,
request_kind: RequestArgumentKind,
) {
render_operation_xml_doc(output, " ", operation, self.api_plan, false);
output.push_str(" ");
output.push_str(GENERATED_CODE_ATTRIBUTE);
output.push('\n');
output.push_str(" ");
output.push_str(self.operation_request_method_access(operation, request_kind));
output.push(' ');
if raw_return == "void" {
output.push_str("async Task ");
} else {
output.push_str("async Task<");
output.push_str(high_level_return);
output.push_str("> ");
}
output.push_str(method_name);
self.render_operation_model_type_parameters(output, operation);
output.push('(');
if has_input {
output.push_str(input_type);
output.push_str(" request");
}
output.push_str(")\n {\n");
self.render_endpoint_service_request_body(output, operation, has_input, raw_return);
output.push_str(" }\n\n");
}
fn render_endpoint_service_request_body(
&self,
output: &mut String,
operation: &PlannedOperation,
has_input: bool,
raw_return: &str,
) {
if !self.operation_model_parameters(operation).is_empty() {
let service_name = self
.api_plan
.services
.iter()
.find(|service| service.operation(&operation.name).is_some())
.map(|service| service.wire_name.as_str())
.unwrap_or_default();
output.push_str(" var client = Workflow.CreateNexusWorkflowClient(");
output.push_str(&csharp_string_literal(service_name));
output.push_str(", _client.Options);\n");
if raw_return == "void" {
output.push_str(" await client.ExecuteNexusOperationAsync(");
} else {
output.push_str(" var result = await client.ExecuteNexusOperationAsync<");
output.push_str(raw_return);
output.push_str(">(");
}
output.push_str(&csharp_string_literal(&operation.wire_name));
output.push_str(", ");
if has_input {
output.push_str("request");
} else {
output.push_str("null");
}
output.push_str(").ConfigureAwait(true);\n");
if raw_return != "void" {
output.push_str(" return result;\n");
}
return;
}
if raw_return == "void" {
output.push_str(" await _client.ExecuteNexusOperationAsync(svc => svc.");
} else {
output.push_str(" var result = await _client.ExecuteNexusOperationAsync<");
output.push_str(raw_return);
output.push_str(">(svc => svc.");
}
output.push_str(&csharp_type_name(&operation.name));
output.push('(');
if has_input {
output.push_str("request");
}
output.push(')');
output.push_str(").ConfigureAwait(true);\n");
if raw_return == "void" {
return;
}
if operation.output_transform.is_some() || operation.data.output_resource_return.is_some() {
let expression =
operation_transform_expression(operation, "request", "result", self.api_plan);
output.push_str(" return ");
if let Some(resource) = &operation.data.output_resource_return {
output.push_str(&csharp_type_name(&resource.resource_type_name));
output.push_str(".BindClient(");
output.push_str(&expression);
output.push_str(", _client)");
} else {
output.push_str(&expression);
}
output.push_str(";\n");
} else if let Some(resource_type_name) =
self.resource_type_name_for_return_type(&self.operation_return_type(operation))
{
output.push_str(" return ");
output.push_str(&resource_type_name);
output.push_str(".BindClient(result, _client);\n");
} else {
output.push_str(" return result;\n");
}
}
fn render_model_constructor(
&self,
output: &mut String,
access: &str,
type_name: &str,
model: &PlannedModel,
) {
let required_fields = model
.model_fields()
.filter(|(_, field)| field.required)
.collect::<Vec<_>>();
if required_fields.is_empty() {
return;
}
output.push_str(" ");
output.push_str(access);
output.push(' ');
output.push_str(type_name);
output.push('(');
for (index, (field_name, field)) in required_fields.iter().enumerate() {
if index > 0 {
output.push_str(", ");
}
output.push_str(&self.model_field_type(model, field_name, field));
output.push(' ');
output.push_str(&csharp_parameter_name(&field.name));
}
output.push_str(")\n {\n");
for (_, field) in required_fields {
output.push_str(" ");
output.push_str(&field_property_name(field));
output.push_str(" = ");
output.push_str(&csharp_parameter_name(&field.name));
output.push_str(";\n");
}
output.push_str(" }\n\n");
}
fn render_operation_request_extension(
&self,
output: &mut String,
operation: &PlannedOperation,
service_type: &str,
method_name: &str,
has_input: bool,
input_type: &str,
raw_return: &str,
high_level_return: &str,
request_kind: RequestArgumentKind,
endpoint_constant_name: &str,
endpoint_parameter: bool,
) {
render_operation_xml_doc(output, " ", operation, self.api_plan, endpoint_parameter);
output.push_str(" ");
output.push_str(GENERATED_CODE_ATTRIBUTE);
output.push('\n');
output.push_str(" ");
let access = self.operation_request_method_access(operation, request_kind);
if endpoint_parameter && access == "private" {
output.push_str("internal");
} else {
output.push_str(access);
}
output.push_str(" static ");
if raw_return == "void" {
output.push_str("async Task ");
} else {
output.push_str("async Task<");
output.push_str(high_level_return);
output.push_str("> ");
}
output.push_str(method_name);
self.render_operation_model_type_parameters(output, operation);
output.push('(');
let mut has_parameters = false;
if endpoint_parameter {
output.push_str("string endpoint");
has_parameters = true;
}
if has_input {
render_parameter_separator(output, &mut has_parameters);
output.push_str(input_type);
output.push_str(" request");
}
output.push_str(")\n {\n");
if !self.operation_model_parameters(operation).is_empty() {
let service_name = self
.api_plan
.services
.iter()
.find(|service| service.operation(&operation.name).is_some())
.map(|service| service.wire_name.as_str())
.unwrap_or_default();
output.push_str(" var client = Workflow.CreateNexusWorkflowClient(");
output.push_str(&csharp_string_literal(service_name));
output.push_str(", ");
if endpoint_parameter {
output.push_str("endpoint");
} else {
output.push_str(endpoint_constant_name);
}
output.push_str(");\n");
if raw_return == "void" {
output.push_str(" await client.ExecuteNexusOperationAsync(");
} else {
output.push_str(" var result = await client.ExecuteNexusOperationAsync<");
output.push_str(raw_return);
output.push_str(">(");
}
output.push_str(&csharp_string_literal(&operation.wire_name));
output.push_str(", ");
if has_input {
output.push_str("request");
} else {
output.push_str("null");
}
output.push_str(").ConfigureAwait(true);\n");
if raw_return != "void" {
output.push_str(" return result;\n");
}
output.push_str(" }\n\n");
return;
}
output.push_str(" var client = Workflow.CreateNexusWorkflowClient<");
output.push_str(service_type);
output.push_str(">(");
if endpoint_parameter {
output.push_str("endpoint");
} else {
output.push_str(endpoint_constant_name);
}
output.push_str(");\n");
if raw_return == "void" {
output.push_str(" await client.ExecuteNexusOperationAsync(svc => svc.");
} else {
output.push_str(" var result = await client.ExecuteNexusOperationAsync<");
output.push_str(raw_return);
output.push_str(">(svc => svc.");
}
output.push_str(&csharp_type_name(&operation.name));
output.push('(');
if has_input {
output.push_str("request");
}
output.push(')');
output.push_str(").ConfigureAwait(true);\n");
if raw_return == "void" {
output.push_str(" }\n\n");
return;
}
if operation.output_transform.is_some() || operation.data.output_resource_return.is_some() {
let expression =
operation_transform_expression(operation, "request", "result", self.api_plan);
output.push_str(" return ");
if endpoint_parameter && let Some(resource) = &operation.data.output_resource_return {
output.push_str(&csharp_type_name(&resource.resource_type_name));
output.push_str(".BindClient(");
output.push_str(&expression);
output.push_str(", client)");
} else {
output.push_str(&expression);
}
output.push_str(";\n");
} else {
if endpoint_parameter
&& let Some(resource_type_name) =
self.resource_type_name_for_return_type(high_level_return)
{
output.push_str(" return ");
output.push_str(&resource_type_name);
output.push_str(".BindClient(result, client);\n");
} else {
output.push_str(" return result;\n");
}
}
output.push_str(" }\n\n");
}
fn resource_type_name_for_return_type(&self, return_type: &str) -> Option<String> {
self.api_plan
.services
.iter()
.flat_map(|service| service.resources.iter().map(|resource| &resource.data))
.map(|resource| csharp_type_name(&resource.type_name))
.find(|resource_type_name| resource_type_name == return_type)
}
fn render_resource_method_inline_operation_body(
&self,
output: &mut String,
operation: &PlannedOperation,
request_plan: &RequestPlan,
) {
let raw_return = self.operation_raw_return_type(operation);
let has_input = self.operation_has_input(operation);
if has_input {
let request_expr = resource_method_request_model_init_expr(
request_plan,
self.api_plan,
None,
ResourceMethodRequestInitKind::AllFields,
)
.or_else(|| {
resource_method_operation_call_args(operation, request_plan, self.api_plan)
.and_then(|args| (args.len() == 1).then(|| args[0].clone()))
})
.expect("bound resource operation should have a renderable request plan");
output.push_str(" var request = ");
output.push_str(&request_expr);
output.push_str(";\n");
}
if raw_return == "void" {
output.push_str(
" await RequireNexgenClient().ExecuteNexusOperationAsync(svc => svc.",
);
} else {
output.push_str(
" var result = await RequireNexgenClient().ExecuteNexusOperationAsync<",
);
output.push_str(&raw_return);
output.push_str(">(svc => svc.");
}
output.push_str(&csharp_type_name(&operation.name));
output.push('(');
if has_input {
output.push_str("request");
}
output.push(')');
output.push_str(").ConfigureAwait(true);\n");
if raw_return == "void" {
return;
}
if operation.output_transform.is_some() || operation.data.output_resource_return.is_some() {
let expression =
operation_transform_expression(operation, "request", "result", self.api_plan);
output.push_str(" return ");
if let Some(resource) = &operation.data.output_resource_return {
output.push_str(&csharp_type_name(&resource.resource_type_name));
output.push_str(".BindClient(");
output.push_str(&expression);
output.push_str(", RequireNexgenClient())");
} else {
output.push_str(&expression);
}
output.push_str(";\n");
} else if let Some(resource_type_name) =
self.resource_type_name_for_return_type(&self.operation_return_type(operation))
{
output.push_str(" return ");
output.push_str(&resource_type_name);
output.push_str(".BindClient(result, RequireNexgenClient());\n");
} else {
output.push_str(" return result;\n");
}
}
fn operation_request_method_access(
&self,
operation: &PlannedOperation,
request_kind: RequestArgumentKind,
) -> &'static str {
if !self.operation_has_input(operation) {
return "public";
}
let raw_input_type = self.operation_raw_input_type(operation.input_model());
let high_level_input_type = self.operation_input_type(operation.input_model());
if (matches!(request_kind, RequestArgumentKind::HighLevel)
|| high_level_input_type == raw_input_type)
&& self
.api_plan
.record(match operation.input_model() {
PlannedType::Record(record) => &record.full_name,
_ => return "public",
})
.is_some_and(|model| {
operation_has_flattened_convenience(operation, model, self.api_plan)
})
{
"private"
} else {
"public"
}
}
fn model_access(&self, model: &PlannedModel) -> &'static str {
if !self
.api_plan
.record_type_parameters(&model.full_name, Language::Dotnet)
.is_empty()
|| self.public_model_names().contains(&model.full_name)
{
"public"
} else {
"internal"
}
}
fn public_model_names(&self) -> HashSet<String> {
let mut names = HashSet::new();
names.extend(self.api_plan.records().filter_map(|(_, record)| {
(!self
.api_plan
.record_type_parameters(&record.full_name, Language::Dotnet)
.is_empty())
.then(|| record.full_name.clone())
}));
for service in &self.api_plan.services {
for operation in &service.operations {
self.collect_public_operation_models(&mut names, operation);
}
for resource in &service.resources {
collect_public_resource_models(&mut names, &resource.data, self.api_plan);
}
}
loop {
let before = names.len();
for name in names.clone() {
let Some(model) = self.api_plan.record(&name) else {
continue;
};
for (_, field) in model.public_fields() {
collect_public_field_kind_models(&mut names, &field.field_type, self.api_plan);
}
}
if names.len() == before {
break;
}
}
names
}
fn collect_public_operation_models(
&self,
names: &mut HashSet<String>,
operation: &PlannedOperation,
) {
let has_input = self.operation_has_input(operation);
let raw_input_type = has_input
.then(|| self.operation_raw_input_type(operation.input_model()))
.unwrap_or_default();
let high_level_input_type = has_input
.then(|| self.operation_input_type(operation.input_model()))
.unwrap_or_default();
if has_input {
let request_kind = if high_level_input_type == raw_input_type {
RequestArgumentKind::Raw
} else {
RequestArgumentKind::HighLevel
};
if self.operation_request_method_access(operation, request_kind) == "public" {
collect_public_operation_input_models(
names,
operation.input_model(),
&high_level_input_type,
self.api_plan,
);
}
}
if let Some(
model_type @ (PlannedType::External(ExternalTypeSpec::Proto(
PlannedProtoType::Message(_),
))
| PlannedType::External(ExternalTypeSpec::Json(_))
| PlannedType::Record(_)),
) = &operation.output
&& operation.output_transform.is_none()
&& operation.data.output_resource_return.is_none()
&& self.operation_return_type(operation)
== csharp_type_name(match model_type {
PlannedType::External(ExternalTypeSpec::Proto(PlannedProtoType::Message(
proto,
))) => &proto.model_name,
PlannedType::External(ExternalTypeSpec::Json(json)) => &json.model_name,
PlannedType::Record(record) => &record.model_name,
_ => unreachable!("model type pattern checked"),
})
{
collect_public_message_models(names, model_type, self.api_plan);
}
if !has_input {
return;
}
let PlannedType::Record(input_record) = operation.input_model() else {
return;
};
let Some(model) = self.api_plan.record(&input_record.full_name) else {
return;
};
if !operation_has_flattened_convenience(operation, model, self.api_plan) {
return;
}
for overload in flattened_overloads(model) {
collect_public_flattened_parameter_models(names, model, self.api_plan, &overload);
}
collect_public_operation_options_models(names, model, self.api_plan);
}
fn operation_has_input(&self, operation: &PlannedOperation) -> bool {
operation_has_input(operation, self.api_plan)
}
fn operation_input_type(&self, model_type: &PlannedType) -> String {
self.external_models
.public_model_type(model_type, self.api_plan)
}
fn operation_model_parameters(
&self,
operation: &PlannedOperation,
) -> Vec<crate::spec::TypeParameterUsage> {
operation
.input_type()
.map(|input| self.api_plan.type_parameters(input, Language::Dotnet))
.unwrap_or_default()
}
fn render_operation_model_type_parameters(
&self,
output: &mut String,
operation: &PlannedOperation,
) {
let parameters = self.operation_model_parameters(operation);
if !parameters.is_empty() {
output.push('<');
output.push_str(
¶meters
.iter()
.map(|usage| usage.parameter.name.as_str())
.collect::<Vec<_>>()
.join(", "),
);
output.push('>');
}
}
fn dotnet_erased_model_type(&self, model_type: &PlannedType) -> String {
if let PlannedType::Record(record) = model_type {
let base = csharp_type_name(&record.model_name);
let parameters = self
.api_plan
.record_type_parameters(&record.full_name, Language::Dotnet);
if !parameters.is_empty() {
return format!(
"{}<{}>",
base,
std::iter::repeat_n("object", parameters.len())
.collect::<Vec<_>>()
.join(", ")
);
}
}
self.operation_input_type(model_type)
}
fn operation_return_type(&self, operation: &PlannedOperation) -> String {
if let Some(transform) = &operation.output_transform
&& let Some(type_name) = transform.type_name.for_language(Language::Dotnet)
{
return type_name.to_string();
}
if let Some(resource) = &operation.data.output_resource_return {
return csharp_type_name(&resource.resource_type_name);
}
self.operation_output_type(operation)
}
fn operation_raw_input_type(&self, model_type: &PlannedType) -> String {
if let PlannedType::Record(record) = model_type
&& !self
.api_plan
.record_type_parameters(&record.full_name, Language::Dotnet)
.is_empty()
{
return self.operation_input_type(model_type);
}
self.external_models
.wire_model_type(model_type, self.api_plan)
}
fn operation_raw_return_type(&self, operation: &PlannedOperation) -> String {
self.operation_output_type(operation)
}
fn operation_registry_request_type(&self, operation: &PlannedOperation) -> String {
if self.operation_has_input(operation) {
self.dotnet_erased_model_type(operation.input_model())
} else {
"object".to_string()
}
}
fn operation_registry_response_type(&self, operation: &PlannedOperation) -> String {
if let Some(output_type @ PlannedType::Record(_)) = operation.output_type() {
return self.dotnet_erased_model_type(output_type);
}
let response_type = self.operation_raw_return_type(operation);
if response_type == "void" {
"object".to_string()
} else {
response_type
}
}
fn operation_output_type(&self, operation: &PlannedOperation) -> String {
if let Some(PlannedType::Record(record)) = operation.output_type() {
let parameters = self
.api_plan
.record_type_parameters(&record.full_name, Language::Dotnet);
if !parameters.is_empty() {
let input_parameters = self
.operation_model_parameters(operation)
.into_iter()
.map(|usage| (usage.parameter.full_name, usage.parameter.name))
.collect::<std::collections::BTreeMap<_, _>>();
let arguments = parameters
.iter()
.map(|usage| {
input_parameters
.get(&usage.parameter.full_name)
.map(String::as_str)
.unwrap_or("object")
})
.collect::<Vec<_>>()
.join(", ");
return format!("{}<{arguments}>", csharp_type_name(&record.model_name));
}
}
match &operation.output {
Some(
model_type @ (PlannedType::External(ExternalTypeSpec::Proto(
PlannedProtoType::Message(_),
))
| PlannedType::External(ExternalTypeSpec::Json(_))
| PlannedType::Record(_)),
) => self
.external_models
.model_type_annotation(model_type)
.unwrap_or_else(|| {
csharp_type_name(match model_type {
PlannedType::External(ExternalTypeSpec::Proto(
PlannedProtoType::Message(proto),
)) => &proto.model_name,
PlannedType::External(ExternalTypeSpec::Json(json)) => &json.model_name,
PlannedType::Record(record) => &record.model_name,
_ => unreachable!("model type pattern checked"),
})
}),
Some(PlannedType::Resource(resource)) => resource
.wire_type
.as_ref()
.map(|wire_type| {
let wire_type = PlannedType::External(wire_type.clone());
self.external_models
.model_type_annotation(&wire_type)
.expect("resource wire output should have a .NET type annotation")
})
.unwrap_or_else(|| csharp_type_name(&resource.type_name)),
None => "void".to_string(),
Some(_) => {
panic!("planned operation output should be proto, record, resource, or none")
}
}
}
fn field_type(&self, field: &RecordFieldSpec<PlannedFamily>) -> String {
let base = self.high_level_field_kind_type(&field.field_type);
if field.required {
base
} else {
nullable_type(&base)
}
}
fn model_field_type(
&self,
model: &PlannedModel,
field_name: &str,
field: &RecordFieldSpec<PlannedFamily>,
) -> String {
if model.function_for_args_field(field_name).is_some()
&& let Some(base) = function_args_parameter_type(
model,
field_name,
self.external_models.function_args_authored_type(field),
)
{
return if field.required {
base
} else {
nullable_type(&base)
};
}
self.field_type(field)
}
fn high_level_field_kind_type(&self, kind: &PlannedType) -> String {
match kind {
PlannedType::List(value) => {
format!("IReadOnlyList<{}>", self.high_level_value_type(value))
}
PlannedType::Map(key, value) => format!(
"IReadOnlyDictionary<{}, {}>",
self.high_level_value_type(key),
self.high_level_value_type(value)
),
value => self.high_level_value_type(value),
}
}
fn high_level_value_type(&self, value: &PlannedType) -> String {
match value {
model_type @ (PlannedType::External(ExternalTypeSpec::Proto(
PlannedProtoType::Message(_),
))
| PlannedType::External(ExternalTypeSpec::Json(_))
| PlannedType::Record(_)) => self
.external_models
.public_model_type(model_type, self.api_plan),
_ => self.dotnet_value_type(value),
}
}
fn resource_field_type(&self, kind: &PlannedType, optional: bool) -> String {
let base = self.dotnet_field_kind_type(kind);
if optional { nullable_type(&base) } else { base }
}
fn resource_method_return_type(&self, method: &PlannedResourceMethod) -> String {
let Some(result) = &method.result else {
return "void".to_string();
};
let base = match &result.kind {
PlannedResourceMethodResultKind::Resource { type_name } => csharp_type_name(type_name),
PlannedResourceMethodResultKind::Value(kind) => self.dotnet_field_kind_type(kind),
};
if result.optional {
nullable_type(&base)
} else {
base
}
}
fn resource_method_task_return_type(&self, method: &PlannedResourceMethod) -> String {
let return_type = self.resource_method_return_type(method);
if return_type == "void" {
"Task".to_string()
} else {
format!("Task<{return_type}>")
}
}
fn dotnet_field_kind_type(&self, kind: &PlannedType) -> String {
match kind {
PlannedType::List(value) => format!("IReadOnlyList<{}>", self.dotnet_value_type(value)),
PlannedType::Map(key, value) => format!(
"IReadOnlyDictionary<{}, {}>",
self.dotnet_value_type(key),
self.dotnet_value_type(value)
),
value => self.dotnet_value_type(value),
}
}
fn dotnet_value_type(&self, value: &PlannedType) -> String {
match value {
PlannedType::Float => "double".to_string(),
PlannedType::Int(IntSpec::I32) => "int".to_string(),
PlannedType::Int(IntSpec::I64) => "long".to_string(),
PlannedType::Bool => "bool".to_string(),
PlannedType::String => "string".to_string(),
PlannedType::Bytes => "byte[]".to_string(),
PlannedType::TypeParameter(parameter) => parameter.name.clone(),
PlannedType::Enum(enumeration) => csharp_type_name(&enumeration.name),
proto_type @ PlannedType::External(ExternalTypeSpec::Proto(PlannedProtoType::Enum(
_,
))) => self
.external_models
.model_type_annotation(proto_type)
.expect("proto enum should have a .NET type annotation"),
PlannedType::Flags(flags) => csharp_type_name(&flags.name),
PlannedType::Variant(variant) => {
let base = csharp_type_name(&variant.name);
let parameters = self
.api_plan
.variant_type_parameters(&variant.full_name, Language::Dotnet);
if parameters.is_empty() {
base
} else {
format!(
"{}<{}>",
base,
parameters
.iter()
.map(|usage| usage.parameter.name.as_str())
.collect::<Vec<_>>()
.join(", ")
)
}
}
model_type @ (PlannedType::External(ExternalTypeSpec::Proto(
PlannedProtoType::Message(_),
))
| PlannedType::External(ExternalTypeSpec::Json(_))
| PlannedType::Record(_)) => self
.external_models
.model_type_annotation(model_type)
.unwrap_or_else(|| match model_type {
PlannedType::External(ExternalTypeSpec::Proto(PlannedProtoType::Message(
proto,
))) => csharp_type_name(&proto.model_name),
PlannedType::External(ExternalTypeSpec::Json(json)) => {
csharp_type_name(&json.model_name)
}
PlannedType::Record(record) => {
let base = csharp_type_name(&record.model_name);
let parameters = self
.api_plan
.record_type_parameters(&record.full_name, Language::Dotnet);
if parameters.is_empty() {
base
} else {
format!(
"{}<{}>",
base,
parameters
.iter()
.map(|usage| usage.parameter.name.as_str())
.collect::<Vec<_>>()
.join(", ")
)
}
}
_ => unreachable!("model type pattern checked"),
}),
PlannedType::Resource(resource) => csharp_type_name(&resource.type_name),
PlannedType::Option(inner) => nullable_type(&self.dotnet_value_type(inner)),
PlannedType::List(inner) => format!("IReadOnlyList<{}>", self.dotnet_value_type(inner)),
PlannedType::Map(key, value) => format!(
"IReadOnlyDictionary<{}, {}>",
self.dotnet_value_type(key),
self.dotnet_value_type(value)
),
PlannedType::Tuple(items) => format!(
"({})",
items
.iter()
.map(|item| self.dotnet_value_type(item))
.collect::<Vec<_>>()
.join(", ")
),
PlannedType::Result { ok, err } => format!(
"NexusResult<{}, {}>",
ok.as_ref()
.map(|ok| self.dotnet_value_type(ok))
.unwrap_or_else(|| "object".to_string()),
err.as_ref()
.map(|err| self.dotnet_value_type(err))
.unwrap_or_else(|| "object".to_string())
),
PlannedType::External(ExternalTypeSpec::Alias {
type_name,
target: fallback,
..
}) => type_name
.for_language(Language::Dotnet)
.map(str::to_string)
.unwrap_or_else(|| self.dotnet_value_type(fallback)),
}
}
fn flattened_parameter_type(
&self,
model: &PlannedModel,
field_name: &str,
field: &RecordFieldSpec<PlannedFamily>,
mode: FlattenedFunctionMode,
) -> String {
if matches!(mode, FlattenedFunctionMode::Expression)
&& let Some(function) = &field.function
{
return function_expression_type(function);
}
if model.function_for_args_field(field_name).is_some() {
let base = function_args_parameter_type(
model,
field_name,
self.external_models.function_args_authored_type(field),
)
.unwrap_or_else(|| self.dotnet_field_kind_type(&field.field_type));
return if field.required {
base
} else {
nullable_type(&base)
};
}
self.field_type(field)
}
fn flattened_nested_parameter_type(
&self,
model: &PlannedModel,
field_name: &str,
field: &RecordFieldSpec<PlannedFamily>,
) -> String {
if let Some(annotation) = model
.field_flattened_annotation(field_name)
.and_then(|annotation| annotation.for_language(Language::Dotnet))
{
if field.required {
annotation.to_string()
} else {
nullable_type(annotation)
}
} else {
self.field_type(field)
}
}
fn render_operation_options_type(&self, output: &mut String, operation: &PlannedOperation) {
if !self.operation_has_input(operation) {
return;
}
let PlannedType::Record(input_record) = operation.input_model() else {
return;
};
let Some(model) = self.api_plan.record(&input_record.full_name) else {
return;
};
if !operation_has_flattened_convenience(operation, model, self.api_plan)
|| !model_has_options_fields(model, self.api_plan)
{
return;
}
render_xml_summary(
output,
"",
dotnet_doc(&operation.doc),
operation.experimental,
);
output.push_str(GENERATED_CODE_ATTRIBUTE);
output.push('\n');
output.push_str("public class ");
let options_type_name = operation_options_type_name(operation);
output.push_str(&options_type_name);
self.render_operation_model_type_parameters(output, operation);
output.push_str("\n{\n");
let mut option_fields = Vec::<(&RecordFieldSpec<PlannedFamily>, String, bool)>::new();
for (field_name, field) in model.public_fields() {
if let Some(nested_model) = flattened_nested_model(field, self.api_plan) {
let _ = field_name;
for (nested_field_name, nested_field) in nested_model.public_fields() {
if !field_is_options_field(nested_model, nested_field_name, nested_field) {
continue;
}
let field_type = self.flattened_nested_parameter_type(
nested_model,
nested_field_name,
nested_field,
);
option_fields.push((
nested_field,
field_type,
field.required && nested_field.required,
));
}
continue;
}
if !field_is_options_field(model, field_name, field) {
continue;
}
let field_type = self.flattened_parameter_type(
model,
field_name,
field,
FlattenedFunctionMode::String,
);
option_fields.push((field, field_type, field.required));
}
render_operation_options_constructor(output, &options_type_name, &option_fields);
for (field, field_type, _) in option_fields {
render_field_xml_doc(output, " ", field);
output.push_str(" public ");
output.push_str(&field_type);
output.push(' ');
output.push_str(&field_property_name(field));
output.push_str(" { get; set; }\n");
}
output.push_str("}\n\n");
}
fn render_operation_flattened_extension(
&self,
output: &mut String,
operation: &PlannedOperation,
method_name: &str,
model: &PlannedModel,
raw_return: &str,
high_level_return: &str,
endpoint_parameter: bool,
instance_method: bool,
operation_call_target: Option<&str>,
endpoint_expr: Option<&str>,
) {
for overload in flattened_overloads(model) {
self.render_operation_flattened_overload(
output,
operation,
method_name,
model,
raw_return,
high_level_return,
&overload,
endpoint_parameter,
instance_method,
operation_call_target,
endpoint_expr,
);
}
}
fn render_operation_flattened_overload(
&self,
output: &mut String,
operation: &PlannedOperation,
method_name: &str,
model: &PlannedModel,
raw_return: &str,
high_level_return: &str,
overload: &FlattenedOverload,
endpoint_parameter: bool,
instance_method: bool,
operation_call_target: Option<&str>,
endpoint_expr: Option<&str>,
) {
self.render_flattened_method_signature(
output,
operation,
method_name,
raw_return,
high_level_return,
model,
overload,
endpoint_parameter,
instance_method,
);
render_flattened_method_body(
output,
operation,
model,
self.api_plan,
overload,
self.support_namespace,
operation_call_target,
endpoint_expr,
);
}
fn render_flattened_method_signature(
&self,
output: &mut String,
operation: &PlannedOperation,
method_name: &str,
raw_return: &str,
high_level_return: &str,
model: &PlannedModel,
overload: &FlattenedOverload,
endpoint_parameter: bool,
instance_method: bool,
) {
render_operation_flattened_xml_doc(
output,
" ",
operation,
model,
self.api_plan,
overload,
);
output.push_str(" ");
output.push_str(GENERATED_CODE_ATTRIBUTE);
output.push('\n');
output.push_str(" public ");
if !instance_method {
output.push_str("static ");
}
if raw_return == "void" {
output.push_str("Task ");
} else {
output.push_str("Task<");
output.push_str(high_level_return);
output.push_str("> ");
}
output.push_str(method_name);
render_flattened_generic_parameters(output, model, overload, self.api_plan);
output.push('(');
let mut has_parameters = false;
if endpoint_parameter {
output.push_str("string endpoint");
has_parameters = true;
}
self.render_flattened_parameters(output, model, overload, &mut has_parameters);
if model_has_options_fields(model, self.api_plan) {
render_parameter_separator(output, &mut has_parameters);
output.push_str(&operation_options_type_name(operation));
self.render_operation_model_type_parameters(output, operation);
if model_options_required(model, self.api_plan) {
output.push_str(" options");
} else {
output.push_str("? options = null");
}
}
output.push_str(")\n {\n");
}
fn render_flattened_parameters(
&self,
output: &mut String,
model: &PlannedModel,
overload: &FlattenedOverload,
has_parameters: &mut bool,
) {
for (field_name, field) in model.public_fields() {
if field.function.is_none() {
continue;
}
if let Some(nested_model) = flattened_nested_model(field, self.api_plan) {
for (nested_field_name, nested_field) in nested_model.public_fields() {
if nested_field.function.is_none() {
continue;
}
let nested_required = field.required && nested_field.required;
render_parameter_separator(output, has_parameters);
output.push_str(&self.flattened_nested_parameter_type(
nested_model,
nested_field_name,
nested_field,
));
output.push(' ');
output.push_str(&csharp_parameter_name(&nested_field.name));
if !nested_required {
output.push_str(" = null");
}
}
continue;
}
render_parameter_separator(output, has_parameters);
output.push_str(&self.flattened_parameter_type(
model,
field_name,
field,
overload.function_mode(field_name),
));
output.push(' ');
output.push_str(&csharp_parameter_name(&field.name));
if !field.required {
output.push_str(" = null");
}
if matches!(
overload.function_mode(field_name),
FlattenedFunctionMode::String
) {
self.render_function_args_parameters(output, model, field, has_parameters);
}
}
}
fn render_function_args_parameters(
&self,
output: &mut String,
model: &PlannedModel,
function_field: &RecordFieldSpec<PlannedFamily>,
has_parameters: &mut bool,
) {
let Some(function) = &function_field.function else {
return;
};
for arg_field_name in &function.arg_fields {
let Some(arg_field) = model.fields.get(arg_field_name) else {
continue;
};
render_parameter_separator(output, has_parameters);
output.push_str(&self.flattened_parameter_type(
model,
arg_field_name,
arg_field,
FlattenedFunctionMode::String,
));
output.push(' ');
output.push_str(&csharp_parameter_name(&arg_field.name));
}
}
}
#[derive(Debug, Default)]
struct DotNetExternalModels {
proto: dotnet_proto::ModelBackend,
json: dotnet_json::ModelBackend,
}
type DotNetExternalModelFragments = dotnet_json::RenderedModelFragments;
impl DotNetExternalModels {
fn new(api_plan: &PlannedSpec) -> Result<Self> {
let mut this = Self::default();
this.prepare(api_plan)?;
Ok(this)
}
fn render_model_wire_methods(
&self,
output: &mut String,
model: &PlannedModel,
api_plan: &PlannedSpec,
support_namespace: Option<&str>,
) -> bool {
self.proto
.render_model_wire_methods(output, model, api_plan, support_namespace)
}
fn model_needs_wire_method(&self, model: &PlannedModel) -> bool {
self.proto.model_needs_wire_method(model)
}
fn model_transfer_converter_attribute(&self, model: &PlannedModel) -> Option<String> {
self.proto.model_transfer_converter_attribute(model)
}
fn model_uses_support_extensions(&self, model: &PlannedModel, api_plan: &PlannedSpec) -> bool {
self.proto.model_uses_support_extensions(model, api_plan)
}
fn function_args_authored_type<'b>(
&self,
field: &'b RecordFieldSpec<PlannedFamily>,
) -> Option<&'b PlannedType> {
match &field.field_type {
PlannedType::External(ExternalTypeSpec::Proto(_)) => {
self.proto.function_args_authored_type(field)
}
_ => None,
}
}
fn public_model_type(&self, model_type: &PlannedType, api_plan: &PlannedSpec) -> String {
if let PlannedType::External(ExternalTypeSpec::Proto(PlannedProtoType::Message(proto))) =
model_type
&& proto.replacement.is_some()
&& let Some(annotation) = self.model_type_annotation(model_type)
{
return annotation;
}
if let PlannedType::Record(record) = model_type
&& api_plan.record(&record.full_name).is_some()
{
let base = csharp_type_name(&record.model_name);
let parameters = api_plan.record_type_parameters(&record.full_name, Language::Dotnet);
return if parameters.is_empty() {
base
} else {
format!(
"{}<{}>",
base,
parameters
.iter()
.map(|usage| usage.parameter.name.as_str())
.collect::<Vec<_>>()
.join(", ")
)
};
}
self.model_type_annotation(model_type).unwrap_or_else(|| {
csharp_type_name(match model_type {
PlannedType::External(ExternalTypeSpec::Proto(PlannedProtoType::Message(
proto,
))) => &proto.model_name,
PlannedType::External(ExternalTypeSpec::Json(json)) => &json.model_name,
PlannedType::Record(record) => &record.model_name,
_ => panic!("model-shaped type should have a model name"),
})
})
}
fn wire_model_type(&self, model_type: &PlannedType, api_plan: &PlannedSpec) -> String {
let planned_record = match model_type {
PlannedType::Record(record) => api_plan.record(&record.full_name),
_ => None,
};
self.wire_type_annotation(model_type, planned_record)
.or_else(|| self.model_type_annotation(model_type))
.unwrap_or_else(|| {
csharp_type_name(match model_type {
PlannedType::External(ExternalTypeSpec::Proto(PlannedProtoType::Message(
proto,
))) => &proto.model_name,
PlannedType::External(ExternalTypeSpec::Json(json)) => &json.model_name,
PlannedType::Record(record) => &record.model_name,
_ => panic!("model-shaped type should have a model name"),
})
})
}
fn wire_type_annotation(
&self,
model_type: &PlannedType,
planned_record: Option<&RecordSpec<PlannedFamily>>,
) -> Option<String> {
match model_type {
PlannedType::External(ExternalTypeSpec::Proto(_)) | PlannedType::Record(_) => {
self.proto.wire_type_annotation(model_type, planned_record)
}
PlannedType::External(ExternalTypeSpec::Json(json_type)) => {
self.json.model_type_annotation(json_type)
}
_ => None,
}
}
fn support_references(&self, value: &PlannedType) -> Vec<String> {
match value {
PlannedType::External(ExternalTypeSpec::Proto(_)) | PlannedType::Record(_) => {
self.proto.support_references(value)
}
PlannedType::External(ExternalTypeSpec::Json(_)) => Vec::new(),
PlannedType::External(ExternalTypeSpec::Alias {
target: fallback, ..
}) => self.support_references(fallback),
PlannedType::Option(inner) | PlannedType::List(inner) => self.support_references(inner),
PlannedType::Map(key, value) => {
let mut references = self.support_references(key);
references.extend(self.support_references(value));
references
}
PlannedType::Tuple(items) => items
.iter()
.flat_map(|item| self.support_references(item))
.collect(),
PlannedType::Result { ok, err } => {
let mut references = ok
.as_deref()
.map(|ok| self.support_references(ok))
.unwrap_or_default();
if let Some(err) = err {
references.extend(self.support_references(err));
}
references
}
_ => Vec::new(),
}
}
}
impl ExternalModelBackend for DotNetExternalModels {
type ModelFragments = DotNetExternalModelFragments;
type WireConversion = WireValueConversion;
fn prepare(&mut self, api_plan: &PlannedSpec) -> Result<()> {
self.proto.prepare(api_plan)?;
self.json.prepare(api_plan)
}
fn render_models(&self) -> Result<DotNetExternalModelFragments> {
self.proto.render_models()?;
self.json.render_models()
}
fn model_type_annotation(&self, model_type: &PlannedType) -> Option<String> {
match model_type {
PlannedType::External(ExternalTypeSpec::Proto(proto_type)) => {
self.proto.model_type_annotation(proto_type)
}
PlannedType::External(ExternalTypeSpec::Json(json_type)) => {
self.json.model_type_annotation(json_type)
}
_ => None,
}
}
fn wire_type_identifier(&self, model_type: &PlannedType) -> Option<String> {
match model_type {
PlannedType::External(ExternalTypeSpec::Proto(proto_type)) => {
self.proto.wire_type_identifier(proto_type)
}
PlannedType::External(ExternalTypeSpec::Json(json_type)) => {
self.json.wire_type_identifier(json_type)
}
_ => None,
}
}
fn wire_conversion(
&self,
model_type: &PlannedType,
planned_record: Option<&RecordSpec<PlannedFamily>>,
) -> Option<WireValueConversion> {
match model_type {
PlannedType::External(ExternalTypeSpec::Proto(_)) | PlannedType::Record(_) => {
self.proto.wire_conversion(model_type, planned_record)
}
PlannedType::External(ExternalTypeSpec::Json(json_type)) => {
self.json.wire_conversion(json_type, planned_record)
}
_ => None,
}
}
}
pub(crate) fn generate(
tree: &crate::spec::ApiSpecTree<PlannedFamily>,
support: &crate::SupportFiles,
mode: GenerationMode,
) -> Result<GeneratedFiles> {
match &tree.root {
ApiSpecNode::Leaf(leaf) => {
let support_fragments = support_fragments_for_plan(&leaf.spec, support);
generate_leaf(&leaf.spec, &support_fragments, mode)
}
ApiSpecNode::Branch(branch) => generate_tree(branch, support, mode),
}
}
fn generate_leaf(
api_plan: &PlannedSpec,
support_fragments: &[SupportFragmentSpec],
mode: GenerationMode,
) -> Result<GeneratedFiles> {
let support_namespace = dotnet_support_namespace(support_fragments)?;
let generator = ApiPlanner::new(api_plan, support_namespace.as_deref())?;
validate_dotnet_support_references(
api_plan,
&generator.external_models,
support_namespace.as_deref(),
)?;
let namespace = dotnet_namespace(api_plan);
let mut files = BTreeMap::<PathBuf, String>::new();
files.insert("Models.cs".into(), generator.render_models_file(&namespace));
if !api_plan.services.is_empty() {
files.insert(
"Services.cs".into(),
generator.render_service_file(&namespace, mode == GenerationMode::NativeApi),
);
}
if mode == GenerationMode::NativeApi
&& api_plan
.services
.iter()
.any(|service| service.endpoint.is_some() && !service.operations.is_empty())
{
files.insert(
"Operations.cs".into(),
generator.render_operations_file(&namespace),
);
}
if api_plan
.services
.iter()
.any(|service| !service.resources.is_empty())
{
files.insert(
"Resources.cs".into(),
generator.render_resources_file(&namespace),
);
}
for fragment in support_fragments {
files.insert(
support_fragment_path(fragment)?,
render_support_file(&fragment.contents),
);
}
Ok(GeneratedFiles::directory(files))
}
fn generate_tree(
branch: &ApiSpecBranch<PlannedFamily>,
support: &crate::SupportFiles,
mode: GenerationMode,
) -> Result<GeneratedFiles> {
let mut files = BTreeMap::new();
let mut warnings = Vec::new();
for node in branch.children.values() {
generate_tree_node(node, support, mode, &mut files, &mut warnings)?;
}
Ok(GeneratedFiles {
layout: crate::generator::GeneratedOutputLayout::Directory,
files,
warnings,
})
}
fn generate_tree_node(
node: &ApiSpecNode<PlannedFamily>,
support: &crate::SupportFiles,
mode: GenerationMode,
files: &mut BTreeMap<PathBuf, String>,
warnings: &mut Vec<String>,
) -> Result<()> {
match node {
ApiSpecNode::Leaf(leaf) => {
let support_fragments = support_fragments_for_plan(&leaf.spec, support);
let generated = generate_leaf(&leaf.spec, &support_fragments, mode)?;
warnings.extend(generated.warnings);
let prefix = leaf.module_path.to_path_buf();
for (path, contents) in generated.files {
insert_generated_file(files, prefix.join(path), contents)?;
}
Ok(())
}
ApiSpecNode::Branch(branch) => {
for node in branch.children.values() {
generate_tree_node(node, support, mode, files, warnings)?;
}
Ok(())
}
}
}
fn insert_generated_file(
files: &mut BTreeMap<PathBuf, String>,
path: PathBuf,
contents: String,
) -> Result<()> {
if files.insert(path.clone(), contents).is_some() {
return Err(Error::GeneratedFileConflict { path });
}
Ok(())
}
fn support_fragments_for_plan(
plan: &PlannedSpec,
support: &crate::SupportFiles,
) -> Vec<SupportFragmentSpec> {
if support.fragments.is_empty() {
plan.support
.fragments_for_language(Language::Dotnet)
.to_vec()
} else {
support.fragments.clone()
}
}
fn dotnet_support_namespace(support_fragments: &[SupportFragmentSpec]) -> Result<Option<String>> {
let mut namespace = None::<String>;
for fragment in support_fragments {
let Some(fragment_namespace) = fragment.namespace.as_deref() else {
continue;
};
validate_dotnet_support_name(fragment_namespace).map_err(|reason| {
Error::InvalidSupportNamespace {
language: Language::Dotnet,
namespace: fragment_namespace.to_string(),
reason,
}
})?;
if let Some(existing) = &namespace {
if existing != fragment_namespace {
return Err(Error::InvalidSupportNamespace {
language: Language::Dotnet,
namespace: fragment_namespace.to_string(),
reason: format!("conflicts with support namespace `{existing}`"),
});
}
} else {
namespace = Some(fragment_namespace.to_string());
}
}
Ok(namespace)
}
fn validate_dotnet_support_references(
api_plan: &PlannedSpec,
external_models: &DotNetExternalModels,
support_namespace: Option<&str>,
) -> Result<()> {
for service in &api_plan.services {
for operation in &service.operations {
if let Some(reference) = operation
.serialization_context
.for_language(Language::Dotnet)
{
validate_dotnet_support_reference(reference, support_namespace)?;
}
}
}
for model in api_plan.records().map(|(_, record)| record) {
for (_, sourced_field, source_expr) in model.sourced_fields() {
if let Some(reference) = source_expr.strip_suffix("()") {
validate_dotnet_support_reference(reference, support_namespace)?;
}
validate_dotnet_field_kind_support_references(
&sourced_field.field_type,
external_models,
support_namespace,
)?;
}
for (_, field) in model.public_fields() {
if let Some(extractor) = field
.function
.as_ref()
.and_then(|function| function.name_extractor.as_deref())
{
validate_dotnet_support_reference(extractor, support_namespace)?;
}
if let Some(extractor) = field
.function
.as_ref()
.and_then(|function| function.call_extractor.as_deref())
{
validate_dotnet_support_reference(extractor, support_namespace)?;
}
validate_dotnet_field_kind_support_references(
&field.field_type,
external_models,
support_namespace,
)?;
}
}
Ok(())
}
fn validate_dotnet_field_kind_support_references(
kind: &PlannedType,
external_models: &DotNetExternalModels,
support_namespace: Option<&str>,
) -> Result<()> {
match kind {
PlannedType::List(value) => {
validate_dotnet_value_support_references(value, external_models, support_namespace)
}
PlannedType::Map(key, value) => {
validate_dotnet_value_support_references(key, external_models, support_namespace)?;
validate_dotnet_value_support_references(value, external_models, support_namespace)
}
value => {
validate_dotnet_value_support_references(value, external_models, support_namespace)
}
}
}
fn validate_dotnet_value_support_references(
value: &PlannedType,
external_models: &DotNetExternalModels,
support_namespace: Option<&str>,
) -> Result<()> {
for reference in external_models.support_references(value) {
validate_dotnet_support_reference(&reference, support_namespace)?;
}
Ok(())
}
fn validate_dotnet_support_reference(
reference: &str,
support_namespace: Option<&str>,
) -> Result<()> {
let qualified = qualify_dotnet_support_reference(reference, support_namespace);
validate_dotnet_support_name(&qualified).map_err(|reason| Error::InvalidSupportNamespace {
language: Language::Dotnet,
namespace: support_namespace.unwrap_or("").to_string(),
reason: format!("invalid support reference `{reference}`: {reason}"),
})
}
fn validate_dotnet_support_name(name: &str) -> std::result::Result<(), String> {
if name.split('.').all(is_valid_csharp_identifier) {
Ok(())
} else {
Err("must be a dotted C# identifier path".to_string())
}
}
fn is_valid_csharp_identifier(name: &str) -> bool {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
(first == '_' || first.is_ascii_alphabetic())
&& chars.all(|character| character == '_' || character.is_ascii_alphanumeric())
}
pub(in crate::generator) fn qualify_dotnet_support_call(
source_expr: &str,
support_namespace: Option<&str>,
) -> String {
if let Some(reference) = source_expr.strip_suffix("()") {
format!(
"{}()",
qualify_dotnet_support_reference(reference, support_namespace)
)
} else {
source_expr.to_string()
}
}
pub(in crate::generator) fn qualify_dotnet_support_reference(
reference: &str,
support_namespace: Option<&str>,
) -> String {
let Some(prefix) = support_namespace.filter(|prefix| !prefix.is_empty()) else {
return reference.to_string();
};
if reference == prefix || reference.starts_with(&format!("{prefix}.")) {
reference.to_string()
} else {
format!("{prefix}.{reference}")
}
}
fn generated_file_prelude(namespace: &str, imports: &[&str]) -> String {
let mut output = String::new();
output.push_str(GENERATED_HEADER);
output.push_str("\n#nullable enable\n");
output.push_str("#pragma warning disable CS1591\n\n");
for import in imports {
output.push_str("using ");
output.push_str(import);
output.push_str(";\n");
}
if !imports.is_empty() {
output.push('\n');
}
output.push_str("namespace ");
output.push_str(namespace);
output.push_str("\n{\n\n");
output
}
fn render_support_file(contents: &str) -> String {
let mut output = String::new();
if !contents.trim_start().starts_with("// <auto-generated") {
output.push_str(GENERATED_HEADER);
output.push('\n');
}
if !contents.contains("#nullable") {
output.push_str("#nullable enable\n");
}
output.push_str(contents);
output
}
fn close_namespace(output: &mut String) {
indent_namespace_body(output);
output.push_str("}\n");
}
fn indent_namespace_body(output: &mut String) {
let Some(body_start) = output.find("\n{\n\n").map(|index| index + "\n{\n\n".len()) else {
return;
};
let body = output[body_start..].to_string();
output.truncate(body_start);
for line in body.split_inclusive('\n') {
if line == "\n" {
output.push('\n');
} else {
output.push_str(" ");
output.push_str(line);
}
}
}
fn dotnet_doc(spec: &crate::spec::LanguageStringSpec) -> Option<&str> {
spec.for_language(Language::Dotnet)
}
fn render_operation_xml_doc(
output: &mut String,
indent: &str,
operation: &PlannedOperation,
api_plan: &PlannedSpec,
endpoint_parameter: bool,
) {
let return_doc = operation
.output
.as_ref()
.and_then(|_| dotnet_doc(&operation.return_doc));
let request_doc = operation_has_input(operation, api_plan)
.then(|| {
let PlannedType::Record(record) = operation.input_model() else {
return None;
};
api_plan.record(&record.full_name)
})
.flatten()
.and_then(|model| dotnet_doc(model.doc()));
let mut params = Vec::new();
if endpoint_parameter {
params.push((
"endpoint".to_string(),
"Endpoint for the service.".to_string(),
));
}
if let Some(request_doc) = request_doc {
params.push(("request".to_string(), request_doc.to_string()));
}
render_xml_doc(
output,
indent,
dotnet_doc(&operation.doc),
params,
return_doc,
operation.experimental,
);
}
fn render_operation_summary_xml_doc(
output: &mut String,
indent: &str,
operation: &PlannedOperation,
) {
let return_doc = operation
.output
.as_ref()
.and_then(|_| dotnet_doc(&operation.return_doc));
render_xml_doc(
output,
indent,
dotnet_doc(&operation.doc),
Vec::new(),
return_doc,
operation.experimental,
);
}
fn render_field_xml_doc(output: &mut String, indent: &str, field: &RecordFieldSpec<PlannedFamily>) {
render_xml_summary(
output,
indent,
field.doc.as_ref().and_then(dotnet_doc),
false,
);
}
fn render_xml_summary(
output: &mut String,
indent: &str,
summary: Option<&str>,
experimental: bool,
) {
render_xml_doc(output, indent, summary, Vec::new(), None, experimental);
}
fn render_xml_doc(
output: &mut String,
indent: &str,
summary: Option<&str>,
params: Vec<(String, String)>,
returns: Option<&str>,
experimental: bool,
) {
if summary.is_none() && params.is_empty() && returns.is_none() && !experimental {
return;
}
if let Some(summary) = summary {
output.push_str(indent);
output.push_str("/// <summary>\n");
render_xml_doc_text(output, indent, summary);
output.push_str(indent);
output.push_str("/// </summary>\n");
}
for (name, doc) in params {
output.push_str(indent);
output.push_str("/// <param name=\"");
output.push_str(&name);
output.push_str("\">");
output.push_str(&xml_doc_escape(doc.trim()));
output.push_str("</param>\n");
}
if let Some(returns) = returns {
output.push_str(indent);
output.push_str("/// <returns>");
output.push_str(&xml_doc_escape(returns.trim()));
output.push_str("</returns>\n");
}
if experimental {
output.push_str(indent);
output.push_str("/// <remarks>");
output.push_str(EXPERIMENTAL_WARNING);
output.push_str("</remarks>\n");
}
}
fn render_xml_doc_text(output: &mut String, indent: &str, text: &str) {
for line in text.trim().lines() {
output.push_str(indent);
output.push_str("/// ");
output.push_str(&xml_doc_escape(line.trim()));
output.push('\n');
}
}
fn xml_doc_escape(text: &str) -> String {
text.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}
#[derive(Clone, Copy)]
enum RequestArgumentKind {
Raw,
HighLevel,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(in crate::generator) struct WireValueConversion {
pub(in crate::generator) annotation: String,
pub(in crate::generator) to_wire: String,
}
fn operation_has_flattened_convenience(
_operation: &PlannedOperation,
model: &PlannedModel,
_api_plan: &PlannedSpec,
) -> bool {
model.public_fields().next().is_some() && model_function_fields_flattenable(model)
}
fn model_function_fields_flattenable(model: &PlannedModel) -> bool {
let mut has_function_fields = false;
for (_, field) in model.public_fields() {
if let Some(function) = &field.function {
has_function_fields = true;
if function.name_extractor.is_none() || function.call_extractor.is_none() {
return false;
}
}
}
!has_function_fields || model_has_extractable_function_fields(model)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum FlattenedFunctionMode {
String,
Expression,
}
struct FlattenedOverload {
function_modes: Vec<(String, FlattenedFunctionMode)>,
}
impl FlattenedOverload {
fn function_mode(&self, field_name: &str) -> FlattenedFunctionMode {
self.function_modes
.iter()
.find_map(|(candidate, mode)| (candidate == field_name).then_some(*mode))
.unwrap_or(FlattenedFunctionMode::String)
}
fn has_expression_functions(&self) -> bool {
self.function_modes
.iter()
.any(|(_, mode)| matches!(mode, FlattenedFunctionMode::Expression))
}
}
fn flattened_overloads(model: &PlannedModel) -> Vec<FlattenedOverload> {
let function_fields = model
.public_fields()
.filter(|(_, field)| field.function.is_some())
.collect::<Vec<_>>();
if function_fields.is_empty() {
return vec![FlattenedOverload {
function_modes: Vec::new(),
}];
}
(0..(1usize << function_fields.len()))
.map(|mask| FlattenedOverload {
function_modes: function_fields
.iter()
.enumerate()
.map(|(index, (field_name, _))| {
let mode = if mask & (1usize << index) == 0 {
FlattenedFunctionMode::String
} else {
FlattenedFunctionMode::Expression
};
((*field_name).to_string(), mode)
})
.collect(),
})
.collect()
}
fn render_operation_flattened_xml_doc(
output: &mut String,
indent: &str,
operation: &PlannedOperation,
model: &PlannedModel,
api_plan: &PlannedSpec,
overload: &FlattenedOverload,
) {
let return_doc = operation
.output
.as_ref()
.and_then(|_| dotnet_doc(&operation.return_doc));
render_xml_doc(
output,
indent,
dotnet_doc(&operation.doc),
flattened_method_parameter_docs(model, api_plan, overload),
return_doc,
operation.experimental,
);
}
fn flattened_method_parameter_docs(
model: &PlannedModel,
api_plan: &PlannedSpec,
overload: &FlattenedOverload,
) -> Vec<(String, String)> {
let mut params = Vec::new();
for (field_name, field) in model.public_fields() {
if field.function.is_none() {
continue;
}
if let Some(nested_model) = flattened_nested_model(field, api_plan) {
for (_, nested_field) in nested_model.public_fields() {
if nested_field.function.is_none() {
continue;
}
push_field_parameter_doc(&mut params, nested_field);
}
continue;
}
push_field_parameter_doc(&mut params, field);
if matches!(
overload.function_mode(field_name),
FlattenedFunctionMode::String
) {
push_function_args_parameter_docs(&mut params, model, field);
}
}
if model_has_options_fields(model, api_plan) {
params.push((
"options".to_string(),
dotnet_doc(model.doc())
.map(str::to_string)
.unwrap_or_else(|| "Options for the operation.".to_string()),
));
}
params
}
fn push_field_parameter_doc(
params: &mut Vec<(String, String)>,
field: &RecordFieldSpec<PlannedFamily>,
) {
params.push((
csharp_parameter_name(&field.name),
field
.doc
.as_ref()
.and_then(dotnet_doc)
.map(str::to_string)
.unwrap_or_else(|| fallback_parameter_doc(field)),
));
}
fn fallback_parameter_doc(field: &RecordFieldSpec<PlannedFamily>) -> String {
format!("The {} value.", csharp_parameter_name(&field.name))
}
fn push_function_args_parameter_docs(
params: &mut Vec<(String, String)>,
model: &PlannedModel,
function_field: &RecordFieldSpec<PlannedFamily>,
) {
let Some(function) = &function_field.function else {
return;
};
for arg_field_name in &function.arg_fields {
let Some(arg_field) = model.fields.get(arg_field_name) else {
continue;
};
push_field_parameter_doc(params, arg_field);
}
}
fn render_parameter_separator(output: &mut String, has_parameters: &mut bool) {
if *has_parameters {
output.push_str(", ");
} else {
*has_parameters = true;
}
}
fn render_flattened_generic_parameters(
output: &mut String,
model: &PlannedModel,
overload: &FlattenedOverload,
api_plan: &PlannedSpec,
) {
let mut generics = api_plan
.record_type_parameters(&model.full_name, Language::Dotnet)
.into_iter()
.map(|usage| usage.parameter.name)
.collect::<Vec<_>>();
if !overload.has_expression_functions() && generics.is_empty() {
return;
}
if overload.has_expression_functions() {
generics.push("TWorkflow".to_string());
}
if overload.has_expression_functions()
&& model.public_fields().any(|(field_name, field)| {
matches!(
overload.function_mode(field_name),
FlattenedFunctionMode::Expression
) && field
.function
.as_ref()
.is_some_and(function_has_result_type_parameter)
})
{
generics.push("TResult".to_string());
}
output.push('<');
output.push_str(&generics.join(", "));
output.push('>');
}
fn render_flattened_method_body(
output: &mut String,
operation: &PlannedOperation,
model: &PlannedModel,
api_plan: &PlannedSpec,
overload: &FlattenedOverload,
support_namespace: Option<&str>,
operation_call_target: Option<&str>,
endpoint_expr: Option<&str>,
) {
let options_required = model_options_required(model, api_plan);
for (field_name, field) in model.public_fields() {
if field.function.is_some()
&& matches!(
overload.function_mode(field_name),
FlattenedFunctionMode::Expression
)
{
output.push_str(" var (");
output.push_str(&csharp_parameter_name(&format!("{}Method", field.name)));
output.push_str(", ");
output.push_str(&csharp_parameter_name(&format!("{}Args", field.name)));
output.push_str(") = ");
let extractor = field
.function
.as_ref()
.and_then(|function| function.call_extractor.as_deref())
.expect("expression overloads require function call extractors");
let extractor = qualify_dotnet_support_reference(extractor, support_namespace);
output.push_str(&extractor);
output.push('(');
output.push_str(&csharp_parameter_name(&field.name));
output.push_str(");\n");
}
}
output.push_str(" var request = new ");
output.push_str(&csharp_type_name(&model.name));
let model_parameters = api_plan.record_type_parameters(&model.full_name, Language::Dotnet);
if !model_parameters.is_empty() {
output.push('<');
output.push_str(
&model_parameters
.iter()
.map(|usage| usage.parameter.name.as_str())
.collect::<Vec<_>>()
.join(", "),
);
output.push('>');
}
let field_exprs = model
.public_fields()
.map(|(field_name, field)| {
(
field_name,
field,
flattened_request_field_expr(
field_name,
field,
model,
api_plan,
overload,
support_namespace,
options_required,
),
)
})
.collect::<Vec<_>>();
output.push('(');
output.push_str(
&field_exprs
.iter()
.filter(|(_, field, _)| field.required)
.map(|(_, _, expr)| expr.as_str())
.collect::<Vec<_>>()
.join(", "),
);
output.push(')');
let optional_field_exprs = field_exprs
.iter()
.filter(|(_, field, _)| !field.required)
.collect::<Vec<_>>();
if optional_field_exprs.is_empty() {
output.push_str(";\n");
} else {
output.push_str("\n {\n");
for (_, field, expr) in optional_field_exprs {
output.push_str(" ");
output.push_str(&field_property_name(field));
output.push_str(" = ");
output.push_str(expr);
output.push_str(",\n");
}
output.push_str(" };\n");
}
output.push_str(" return ");
if let Some(operation_call_target) = operation_call_target {
output.push_str(operation_call_target);
output.push('.');
}
output.push_str(&csharp_type_name(&operation.name));
output.push_str("Async(");
if let Some(endpoint_expr) = endpoint_expr {
output.push_str(endpoint_expr);
output.push_str(", ");
}
output.push_str("request);\n");
output.push_str(" }\n\n");
}
fn flattened_request_field_expr(
field_name: &str,
field: &RecordFieldSpec<PlannedFamily>,
model: &PlannedModel,
api_plan: &PlannedSpec,
overload: &FlattenedOverload,
support_namespace: Option<&str>,
options_required: bool,
) -> String {
if let Some(nested_model) = flattened_nested_model(field, api_plan) {
nested_model_init_expr(field, nested_model, options_required)
} else if field.function.is_some()
&& matches!(
overload.function_mode(field_name),
FlattenedFunctionMode::Expression
)
{
function_name_expression(field, support_namespace)
} else if model.function_for_args_field(field_name).is_some()
&& let Some(mode) = function_args_field_mode(model, field_name, overload)
{
if matches!(mode, FlattenedFunctionMode::Expression) {
let (function_field_name, _) = model
.functions()
.find(|(_, function)| function.arg_fields.iter().any(|arg| arg == field_name))
.expect("function args field should have a function field");
let function_field = model
.fields
.get(function_field_name)
.expect("function field should exist on model");
csharp_parameter_name(&format!("{}Args", function_field.name))
} else {
csharp_parameter_name(&field.name)
}
} else if field.function.is_none() {
option_field_expr(field, options_required)
} else {
csharp_parameter_name(&field.name)
}
}
fn function_name_expression(
field: &RecordFieldSpec<PlannedFamily>,
support_namespace: Option<&str>,
) -> String {
let method_var = csharp_parameter_name(&format!("{}Method", field.name));
let extractor = field
.function
.as_ref()
.and_then(|function| function.name_extractor.as_deref())
.expect("expression overloads require function name extractors");
let extractor = qualify_dotnet_support_reference(extractor, support_namespace);
format!("{extractor}({method_var})")
}
fn function_args_field_mode(
model: &PlannedModel,
args_field_name: &str,
overload: &FlattenedOverload,
) -> Option<FlattenedFunctionMode> {
model
.functions()
.find(|(_, function)| function.arg_fields.iter().any(|arg| arg == args_field_name))
.map(|(function_field_name, _)| overload.function_mode(function_field_name))
}
fn flattened_nested_model<'a>(
field: &RecordFieldSpec<PlannedFamily>,
api_plan: &'a PlannedSpec,
) -> Option<&'a PlannedModel> {
let PlannedType::Record(record) = &field.field_type else {
return None;
};
let nested_model = api_plan.record(&record.full_name)?;
nested_model.flatten_in_api.then_some(nested_model)
}
fn render_operation_options_constructor(
output: &mut String,
type_name: &str,
option_fields: &[(&RecordFieldSpec<PlannedFamily>, String, bool)],
) {
let required_fields = option_fields
.iter()
.filter(|(_, _, required)| *required)
.collect::<Vec<_>>();
if required_fields.is_empty() {
return;
}
output.push_str(" public ");
output.push_str(type_name);
output.push('(');
for (index, (field, field_type, _)) in required_fields.iter().enumerate() {
if index > 0 {
output.push_str(", ");
}
output.push_str(field_type);
output.push(' ');
output.push_str(&csharp_parameter_name(&field.name));
}
output.push_str(")\n {\n");
for (field, _, _) in required_fields {
output.push_str(" ");
output.push_str(&field_property_name(field));
output.push_str(" = ");
output.push_str(&csharp_parameter_name(&field.name));
output.push_str(";\n");
}
output.push_str(" }\n\n");
}
fn operation_options_type_name(operation: &PlannedOperation) -> String {
csharp_type_name(&format!("{}Options", operation.name))
}
fn service_definition_field_name(service: &PlannedService) -> String {
format!("{}ServiceDefinition", csharp_type_name(&service.name))
}
fn model_has_options_fields(model: &PlannedModel, api_plan: &PlannedSpec) -> bool {
model.public_fields().any(|(field_name, field)| {
if let Some(nested_model) = flattened_nested_model(field, api_plan) {
nested_model
.public_fields()
.any(|(nested_field_name, nested_field)| {
field_is_options_field(nested_model, nested_field_name, nested_field)
})
} else {
field_is_options_field(model, field_name, field)
}
})
}
fn model_options_required(model: &PlannedModel, api_plan: &PlannedSpec) -> bool {
model.public_fields().any(|(field_name, field)| {
if let Some(nested_model) = flattened_nested_model(field, api_plan) {
field.required
&& nested_model
.public_fields()
.any(|(nested_field_name, nested_field)| {
nested_field.required
&& field_is_options_field(nested_model, nested_field_name, nested_field)
})
} else {
field.required && field_is_options_field(model, field_name, field)
}
})
}
fn field_is_options_field(
model: &PlannedModel,
field_name: &str,
field: &RecordFieldSpec<PlannedFamily>,
) -> bool {
field.function.is_none() && model.function_for_args_field(field_name).is_none()
}
fn nested_model_init_expr(
field: &RecordFieldSpec<PlannedFamily>,
model: &PlannedModel,
options_required: bool,
) -> String {
let option_fields = model
.public_fields()
.filter(|(nested_field_name, nested_field)| {
field_is_options_field(model, nested_field_name, nested_field)
})
.collect::<Vec<_>>();
let checks = option_fields
.iter()
.map(|(_, nested_field)| {
format!(
"{} != null",
option_nested_field_expr(field, nested_field, options_required)
)
})
.collect::<Vec<_>>()
.join(" || ");
let init_expr = model_init_expr(
&csharp_type_name(&model.name),
option_fields
.into_iter()
.map(|(_, nested_field)| {
(
nested_field,
option_nested_field_expr(field, nested_field, options_required),
)
})
.collect(),
);
if field.required {
init_expr
} else {
format!("{checks} ? {init_expr} : null")
}
}
fn model_init_expr(
type_name: &str,
field_exprs: Vec<(&RecordFieldSpec<PlannedFamily>, String)>,
) -> String {
let constructor_args = field_exprs
.iter()
.filter(|(field, _)| field.required)
.map(|(_, expr)| expr.as_str())
.collect::<Vec<_>>()
.join(", ");
let assignments = field_exprs
.iter()
.filter(|(field, _)| !field.required)
.map(|(field, expr)| format!("{} = {}", field_property_name(field), expr))
.collect::<Vec<_>>()
.join(", ");
let mut expr = format!("new {type_name}({constructor_args})");
if !assignments.is_empty() {
expr.push_str(" { ");
expr.push_str(&assignments);
expr.push_str(" }");
}
expr
}
fn option_field_expr(field: &RecordFieldSpec<PlannedFamily>, options_required: bool) -> String {
if options_required || field.required {
format!("options.{}", field_property_name(field))
} else {
format!("options?.{}", field_property_name(field))
}
}
fn option_nested_field_expr(
field: &RecordFieldSpec<PlannedFamily>,
nested_field: &RecordFieldSpec<PlannedFamily>,
options_required: bool,
) -> String {
if options_required || (field.required && nested_field.required) {
format!("options.{}", field_property_name(nested_field))
} else {
format!("options?.{}", field_property_name(nested_field))
}
}
fn function_has_result_type_parameter(function: &FunctionFieldSpec<PlannedFamily>) -> bool {
function.result_type_parameter.is_some()
}
fn model_has_extractable_function_fields(model: &PlannedModel) -> bool {
let mut has_function_fields = false;
for (_, field) in model.public_fields() {
if let Some(function) = &field.function {
has_function_fields = true;
if function.name_extractor.is_none() {
return false;
}
}
}
has_function_fields
}
fn render_enum(output: &mut String, enumeration: &EnumSpec) {
output.push_str(GENERATED_CODE_ATTRIBUTE);
output.push('\n');
output.push_str("public enum ");
output.push_str(&csharp_type_name(&enumeration.name));
output.push_str("\n{\n");
for value in &enumeration.values {
output.push_str(" ");
output.push_str(&csharp_type_name(&enum_value_name(&value.name)));
output.push_str(" = ");
output.push_str(&value.number.to_string());
output.push_str(",\n");
}
output.push_str("}\n\n");
}
fn render_flags(output: &mut String, flag_set: &PlannedFlags) {
output.push_str(GENERATED_CODE_ATTRIBUTE);
output.push('\n');
output.push_str("[Flags]\n");
output.push_str("public enum ");
output.push_str(&csharp_type_name(&flag_set.name));
output.push_str("\n{\n");
output.push_str(" None = 0,\n");
for flag in &flag_set.flags {
output.push_str(" ");
output.push_str(&csharp_type_name(&flag.name));
output.push_str(" = 1 << ");
output.push_str(&flag.bit.to_string());
output.push_str(",\n");
}
output.push_str("}\n\n");
}
fn collect_public_flattened_parameter_models(
names: &mut HashSet<String>,
model: &PlannedModel,
api_plan: &PlannedSpec,
overload: &FlattenedOverload,
) {
for (field_name, field) in model.public_fields() {
if field.function.is_none() {
continue;
}
if let Some(nested_model) = flattened_nested_model(field, api_plan) {
let _ = field_name;
for (_, nested_field) in nested_model.public_fields() {
if nested_field.function.is_some() {
collect_public_field_kind_models(names, &nested_field.field_type, api_plan);
}
}
continue;
}
collect_public_field_kind_models(names, &field.field_type, api_plan);
if matches!(
overload.function_mode(field_name),
FlattenedFunctionMode::String
) && let Some(function) = &field.function
{
for arg_field_name in &function.arg_fields {
if let Some(arg_field) = model.fields.get(arg_field_name) {
collect_public_field_kind_models(names, &arg_field.field_type, api_plan);
}
}
}
}
}
fn collect_public_operation_options_models(
names: &mut HashSet<String>,
model: &PlannedModel,
api_plan: &PlannedSpec,
) {
if !model_has_options_fields(model, api_plan) {
return;
}
for (field_name, field) in model.public_fields() {
if let Some(nested_model) = flattened_nested_model(field, api_plan) {
let _ = field_name;
for (nested_field_name, nested_field) in nested_model.public_fields() {
if field_is_options_field(nested_model, nested_field_name, nested_field) {
collect_public_field_kind_models(names, &nested_field.field_type, api_plan);
}
}
continue;
}
if field_is_options_field(model, field_name, field) {
collect_public_field_kind_models(names, &field.field_type, api_plan);
}
}
}
fn collect_public_resource_models(
names: &mut HashSet<String>,
resource: &PlannedResource,
api_plan: &PlannedSpec,
) {
for field in &resource.fields {
collect_public_field_kind_models(names, &field.kind, api_plan);
}
for method in &resource.methods {
for param in &method.params {
collect_public_field_kind_models(names, ¶m.kind, api_plan);
}
if let Some(result) = &method.result
&& let PlannedResourceMethodResultKind::Value(kind) = &result.kind
{
collect_public_field_kind_models(names, kind, api_plan);
}
}
}
fn collect_public_field_kind_models(
names: &mut HashSet<String>,
kind: &PlannedType,
api_plan: &PlannedSpec,
) {
match kind {
PlannedType::Option(value) | PlannedType::List(value) => {
collect_public_value_models(names, value, api_plan)
}
PlannedType::Map(key, value) => {
collect_public_value_models(names, key, api_plan);
collect_public_value_models(names, value, api_plan);
}
value => collect_public_value_models(names, value, api_plan),
}
}
fn collect_public_value_models(
names: &mut HashSet<String>,
value: &PlannedType,
api_plan: &PlannedSpec,
) {
collect_public_value_models_inner(names, value, api_plan, &mut HashSet::new());
}
fn collect_public_value_models_inner(
names: &mut HashSet<String>,
value: &PlannedType,
api_plan: &PlannedSpec,
visiting_variants: &mut HashSet<String>,
) {
match value {
PlannedType::Option(inner) | PlannedType::List(inner) => {
collect_public_value_models_inner(names, inner, api_plan, visiting_variants);
}
PlannedType::Map(key, value) => {
collect_public_value_models_inner(names, key, api_plan, visiting_variants);
collect_public_value_models_inner(names, value, api_plan, visiting_variants);
}
model_type @ (PlannedType::External(ExternalTypeSpec::Proto(
PlannedProtoType::Message(_),
))
| PlannedType::Record(_)) => collect_public_message_models(names, model_type, api_plan),
PlannedType::Resource(_) => {}
PlannedType::Variant(variant) => {
if !visiting_variants.insert(variant.full_name.clone()) {
return;
}
if let Some(declaration) = api_plan.variant(&variant.full_name) {
for case in &declaration.cases {
if let Some(payload) = &case.payload {
collect_public_value_models_inner(
names,
payload,
api_plan,
visiting_variants,
);
}
}
}
visiting_variants.remove(&variant.full_name);
}
PlannedType::Tuple(items) => {
for item in items {
collect_public_value_models_inner(names, item, api_plan, visiting_variants);
}
}
PlannedType::Result { ok, err } => {
if let Some(ok) = ok {
collect_public_value_models_inner(names, ok, api_plan, visiting_variants);
}
if let Some(err) = err {
collect_public_value_models_inner(names, err, api_plan, visiting_variants);
}
}
PlannedType::External(ExternalTypeSpec::Alias {
type_name,
target: fallback,
..
}) => {
if type_name.for_language(Language::Dotnet).is_none() {
collect_public_value_models_inner(names, fallback, api_plan, visiting_variants);
}
}
_ => {}
}
}
fn collect_public_message_models(
names: &mut HashSet<String>,
model_type: &PlannedType,
api_plan: &PlannedSpec,
) {
match model_type {
PlannedType::External(ExternalTypeSpec::Proto(PlannedProtoType::Message(proto)))
if proto.replacement.is_some() => {}
PlannedType::Record(record) if api_plan.record(&record.full_name).is_some() => {
names.insert(record.full_name.clone());
}
_ => {}
}
}
fn collect_public_operation_input_models(
names: &mut HashSet<String>,
model_type: &PlannedType,
input_type: &str,
api_plan: &PlannedSpec,
) {
if input_type
== csharp_type_name(match model_type {
PlannedType::External(ExternalTypeSpec::Proto(PlannedProtoType::Message(proto))) => {
&proto.model_name
}
PlannedType::External(ExternalTypeSpec::Json(json)) => &json.model_name,
PlannedType::Record(record) => &record.model_name,
_ => panic!("operation input should be a model"),
})
{
collect_public_message_models(names, model_type, api_plan);
}
}
fn render_resource_method_operation_body(
output: &mut String,
service: &PlannedService,
operation: &PlannedOperation,
request_plan: &RequestPlan,
api_plan: &PlannedSpec,
endpoint_expr: Option<&str>,
) {
let operation_method_name = format!("{}Async", csharp_type_name(&operation.name));
let operation_class_name = dotnet_operations_class_name(service);
let args = resource_method_operation_call_args(operation, request_plan, api_plan)
.expect("bound resource operation should have a renderable request plan");
if args.len() == 1 {
output.push_str(" var request = ");
output.push_str(&args[0]);
output.push_str(";\n");
}
output.push_str(" return ");
output.push_str(&operation_class_name);
output.push('.');
output.push_str(&operation_method_name);
output.push('(');
if let Some(endpoint_expr) = endpoint_expr {
output.push_str(endpoint_expr);
if !args.is_empty() {
output.push_str(", ");
}
}
if args.len() == 1 {
output.push_str("request");
} else {
output.push_str(&args.join(", "));
}
output.push_str(");\n");
}
fn resource_method_operation_call_args(
operation: &PlannedOperation,
request_plan: &RequestPlan,
api_plan: &PlannedSpec,
) -> Option<Vec<String>> {
let model = api_plan_record_for_request_name(
api_plan,
match operation.input_model() {
PlannedType::Record(record) => &record.full_name,
_ => return None,
},
)?;
if operation_has_flattened_convenience(operation, model, api_plan) {
if model
.public_fields()
.any(|(_, field)| field.function.is_some())
{
return resource_method_flattened_operation_call_args(
operation,
request_plan,
model,
api_plan,
);
}
if model_has_options_fields(model, api_plan)
&& let Some(expr) = resource_method_request_model_init_expr(
request_plan,
api_plan,
Some(&operation_options_type_name(operation)),
ResourceMethodRequestInitKind::OptionsFields,
)
{
return Some(vec![expr]);
}
}
resource_method_request_model_init_expr(
request_plan,
api_plan,
None,
ResourceMethodRequestInitKind::AllFields,
)
.map(|expr| vec![expr])
}
fn resource_method_flattened_operation_call_args(
operation: &PlannedOperation,
request_plan: &RequestPlan,
model: &PlannedModel,
api_plan: &PlannedSpec,
) -> Option<Vec<String>> {
let source_exprs = resource_method_request_field_exprs(request_plan, model, api_plan)?;
let overload = flattened_overloads(model)
.into_iter()
.find(|overload| !overload.has_expression_functions())?;
let mut args = Vec::new();
for (field_name, field) in model.public_fields() {
if field.function.is_none() {
continue;
}
if flattened_nested_model(field, api_plan).is_some() {
return None;
}
args.push(resource_method_request_source_expr(
&source_exprs,
field_name,
field,
)?);
if matches!(
overload.function_mode(field_name),
FlattenedFunctionMode::String
) && let Some(function) = &field.function
{
for arg_field_name in &function.arg_fields {
let arg_field = model.fields.get(arg_field_name)?;
args.push(
resource_method_request_source_expr(&source_exprs, arg_field_name, arg_field)
.or_else(|| (!arg_field.required).then(|| "null".to_string()))?,
);
}
}
}
if model_has_options_fields(model, api_plan) {
args.push(resource_method_request_model_init_expr(
request_plan,
api_plan,
Some(&operation_options_type_name(operation)),
ResourceMethodRequestInitKind::OptionsFields,
)?);
}
Some(args)
}
#[derive(Clone, Copy)]
enum ResourceMethodRequestInitKind {
AllFields,
OptionsFields,
}
fn resource_method_request_model_init_expr(
request_plan: &RequestPlan,
api_plan: &PlannedSpec,
type_name_override: Option<&str>,
init_kind: ResourceMethodRequestInitKind,
) -> Option<String> {
let RequestPlan::Construct {
message_name,
fields,
} = request_plan
else {
return None;
};
let model = api_plan_record_for_request_name(api_plan, message_name)?;
let mut field_exprs = Vec::new();
for field in fields {
let (model_field_name, model_field) =
model.public_fields().find(|(field_name, candidate)| {
candidate.name == field.field_name || *field_name == field.field_name
})?;
if matches!(init_kind, ResourceMethodRequestInitKind::OptionsFields)
&& !field_is_options_field(model, model_field_name, model_field)
{
continue;
}
field_exprs.push((
model_field,
resource_method_request_value_expr(&field.value, api_plan)?,
));
}
Some(model_init_expr(
type_name_override.unwrap_or(&csharp_type_name(&model.name)),
field_exprs,
))
}
fn api_plan_record_for_request_name<'a>(
api_plan: &'a PlannedSpec,
name: &str,
) -> Option<&'a PlannedModel> {
api_plan.record(name).or_else(|| {
let name = name.trim_start_matches('.');
api_plan.records().map(|(_, record)| record).find(|record| {
matches!(
record.source_type.as_ref(),
Some(ExternalTypeSpec::Proto(PlannedProtoType::Message(message)))
if message.proto.full_name == name
)
})
})
}
fn resource_method_request_field_exprs(
request_plan: &RequestPlan,
model: &PlannedModel,
api_plan: &PlannedSpec,
) -> Option<BTreeMap<String, String>> {
let RequestPlan::Construct { fields, .. } = request_plan else {
return None;
};
let mut source_exprs = BTreeMap::new();
for field in fields {
let (model_field_name, model_field) =
model.public_fields().find(|(field_name, candidate)| {
candidate.name == field.field_name || *field_name == field.field_name
})?;
let expr = resource_method_request_value_expr(&field.value, api_plan)?;
source_exprs.insert(model_field.name.clone(), expr.clone());
source_exprs.insert(model_field_name.to_string(), expr);
}
Some(source_exprs)
}
fn resource_method_request_source_expr(
source_exprs: &BTreeMap<String, String>,
field_name: &str,
field: &RecordFieldSpec<PlannedFamily>,
) -> Option<String> {
source_exprs
.get(&field.name)
.or_else(|| source_exprs.get(field_name))
.cloned()
}
fn resource_method_request_value_expr(
request_plan: &RequestPlan,
api_plan: &PlannedSpec,
) -> Option<String> {
match request_plan {
RequestPlan::Source(RequestPlanSource::ResourceField(name)) => Some(csharp_type_name(name)),
RequestPlan::Source(RequestPlanSource::MethodParam(name)) => {
Some(csharp_parameter_name(name))
}
RequestPlan::Construct { .. } => resource_method_request_model_init_expr(
request_plan,
api_plan,
None,
ResourceMethodRequestInitKind::AllFields,
),
}
}
fn render_result_helper(output: &mut String) {
output.push_str(GENERATED_CODE_ATTRIBUTE);
output.push('\n');
output.push_str("public sealed class NexusResult<TOk, TErr>\n{\n");
output.push_str(" private NexusResult(bool isOk, TOk? ok, TErr? err)\n");
output.push_str(" {\n IsOk = isOk;\n Ok = ok;\n Err = err;\n }\n\n");
output.push_str(" public bool IsOk { get; }\n");
output.push_str(" public TOk? Ok { get; }\n");
output.push_str(" public TErr? Err { get; }\n\n");
output.push_str(" public static NexusResult<TOk, TErr> FromOk(TOk value) => new(true, value, default);\n");
output.push_str(" public static NexusResult<TOk, TErr> FromErr(TErr value) => new(false, default, value);\n");
output.push_str("}\n\n");
}
fn operation_has_input(operation: &PlannedOperation, api_plan: &PlannedSpec) -> bool {
let Some(input) = operation.input.as_ref() else {
return false;
};
match input {
PlannedType::Record(record) => api_plan
.record(&record.full_name)
.is_none_or(|model| model.public_fields().next().is_some()),
PlannedType::External(ExternalTypeSpec::Proto(PlannedProtoType::Message(_)))
| PlannedType::External(ExternalTypeSpec::Json(_)) => true,
_ => panic!("operation input should be a model"),
}
}
fn operation_transform_expression(
operation: &PlannedOperation,
request_expr: &str,
result_expr: &str,
api_plan: &PlannedSpec,
) -> String {
if let Some(transform) = &operation.output_transform
&& let Some(expression) = transform.transform.for_language(Language::Dotnet)
{
return expression
.replace("request", request_expr)
.replace("result", result_expr);
}
if let Some(resource) = &operation.data.output_resource_return {
let args = resource
.bindings
.iter()
.map(|binding| {
resource_return_binding_expr(resource, binding, request_expr, result_expr, api_plan)
})
.collect::<Vec<_>>();
return format!(
"new {}({})",
csharp_type_name(&resource.resource_type_name),
args.join(", ")
);
}
result_expr.to_string()
}
fn resource_return_binding_expr(
resource: &crate::planning::PlannedOperationResourceReturn,
binding: &PlannedOperationResourceFieldBinding,
request_expr: &str,
result_expr: &str,
api_plan: &PlannedSpec,
) -> String {
match &binding.source {
ResolvedResourceBindingSource::RequestField {
field_name,
proto_field_name: _,
hidden,
} => {
if *hidden {
let expr = format!("{request_expr}.{}", csharp_type_name(field_name));
optional_resource_binding_expr(resource, binding, expr, api_plan)
} else {
format!("{request_expr}.{}", csharp_type_name(field_name))
}
}
ResolvedResourceBindingSource::ResultField {
proto_field_name: wire_field_name,
..
} => {
let expr = format!("{result_expr}.{}", csharp_type_name(wire_field_name));
optional_resource_binding_expr(resource, binding, expr, api_plan)
}
}
}
fn optional_resource_binding_expr(
resource: &crate::planning::PlannedOperationResourceReturn,
binding: &PlannedOperationResourceFieldBinding,
expr: String,
api_plan: &PlannedSpec,
) -> String {
if binding.optional && resource_field_is_string(resource, &binding.field_name, api_plan) {
format!("string.IsNullOrEmpty({expr}) ? null : {expr}")
} else {
expr
}
}
fn resource_field_is_string(
resource_return: &crate::planning::PlannedOperationResourceReturn,
field_name: &str,
api_plan: &PlannedSpec,
) -> bool {
let Some(field_kind) = api_plan
.services
.iter()
.flat_map(|service| service.resources.iter().map(|resource| &resource.data))
.find(|resource| {
resource.type_name == resource_return.resource_type_name
|| csharp_type_name(&resource.name) == resource_return.resource_type_name
})
.and_then(|resource| {
resource
.fields
.iter()
.find(|field| field.name == field_name)
})
.map(|field| &field.kind)
else {
return false;
};
matches!(field_kind, PlannedType::String)
}
pub(in crate::generator) fn function_args_parameter_type(
model: &PlannedModel,
field_name: &str,
authored_type_override: Option<&PlannedType>,
) -> Option<String> {
if let Some(field_type) = model.field_type(field_name)
&& !authored_type_is_direct_reference(field_type.without_option())
{
return Some(dotnet_function_args_wit_type(field_type));
}
authored_type_override.map(dotnet_function_args_wit_type)
}
fn authored_type_is_direct_reference(authored_type: &PlannedType) -> bool {
matches!(
authored_type,
TypeSpec::Record(_)
| TypeSpec::Enum(_)
| TypeSpec::Flags(_)
| TypeSpec::Variant(_)
| TypeSpec::Resource(_)
| TypeSpec::External(ExternalTypeSpec::Proto(_))
)
}
fn dotnet_function_args_wit_type(wit_type: &PlannedType) -> String {
match wit_type.without_option() {
TypeSpec::List(inner) => format!(
"IReadOnlyCollection<{}>",
dotnet_authored_type(inner.without_option())
),
other => dotnet_authored_type(other),
}
}
fn dotnet_authored_type(wit_type: &PlannedType) -> String {
match wit_type {
TypeSpec::Bool => "bool".to_string(),
TypeSpec::Int(IntSpec::I32) => "int".to_string(),
TypeSpec::Int(IntSpec::I64) => "long".to_string(),
TypeSpec::Float => "double".to_string(),
TypeSpec::String => "string".to_string(),
TypeSpec::Bytes => "byte[]".to_string(),
TypeSpec::TypeParameter(parameter) => parameter.name.clone(),
TypeSpec::Option(inner) => nullable_type(&dotnet_authored_type(inner)),
TypeSpec::List(inner) => {
format!("IReadOnlyList<{}>", dotnet_authored_type(inner))
}
TypeSpec::Tuple(items) => format!(
"({})",
items
.iter()
.map(dotnet_authored_type)
.collect::<Vec<_>>()
.join(", ")
),
TypeSpec::Map(key, value) => format!(
"IReadOnlyDictionary<{}, {}>",
dotnet_authored_type(key),
dotnet_authored_type(value)
),
TypeSpec::Result { ok, err } => format!(
"NexusResult<{}, {}>",
ok.as_ref()
.map(|ok| dotnet_authored_type(ok))
.unwrap_or_else(|| "object".to_string()),
err.as_ref()
.map(|err| dotnet_authored_type(err))
.unwrap_or_else(|| "object".to_string())
),
TypeSpec::External(ExternalTypeSpec::Proto(proto_name)) => {
dotnet_proto::dotnet_proto_type_name_fallback(proto_name.full_name())
}
TypeSpec::External(ExternalTypeSpec::Json(json_type)) => {
csharp_type_name(&json_type.model_name)
}
TypeSpec::Record(name) => csharp_type_name(&name.full_name),
TypeSpec::Enum(name) => csharp_type_name(&name.full_name),
TypeSpec::Flags(name) => csharp_type_name(&name.full_name),
TypeSpec::Variant(name) => csharp_type_name(&name.full_name),
TypeSpec::Resource(name) => csharp_type_name(&name.type_name),
TypeSpec::External(ExternalTypeSpec::Alias {
target, type_name, ..
}) => type_name
.for_language(Language::Dotnet)
.map(str::to_string)
.unwrap_or_else(|| dotnet_authored_type(target)),
}
}
fn function_expression_type(function: &FunctionFieldSpec<PlannedFamily>) -> String {
if function_has_result_type_parameter(function) {
"Expression<Func<TWorkflow, Task<TResult>>>".to_string()
} else {
"Expression<Func<TWorkflow, Task>>".to_string()
}
}
fn nullable_type(base: &str) -> String {
if base.ends_with('?') {
base.to_string()
} else {
format!("{base}?")
}
}
fn field_kind_uses_result(kind: &PlannedType) -> bool {
value_uses_result(kind)
}
fn value_uses_result(value: &PlannedType) -> bool {
match value {
PlannedType::Result { .. } => true,
PlannedType::List(inner) => value_uses_result(inner),
PlannedType::Map(key, value) => value_uses_result(key) || value_uses_result(value),
PlannedType::Tuple(items) => items.iter().any(value_uses_result),
PlannedType::External(ExternalTypeSpec::Alias {
target: fallback, ..
}) => value_uses_result(fallback),
_ => false,
}
}
fn support_fragment_path(fragment: &SupportFragmentSpec) -> Result<PathBuf> {
let path = PathBuf::from(&fragment.path);
if path.extension().and_then(|extension| extension.to_str()) != Some("cs") {
return Err(Error::InvalidGeneratedPath {
path,
reason: ".NET support files must end with `.cs`".to_string(),
});
}
let file_name = path
.file_name()
.ok_or_else(|| Error::InvalidGeneratedPath {
path: path.clone(),
reason: "support path must have a file name".to_string(),
})?
.to_os_string();
Ok(PathBuf::from("Support").join(file_name))
}
fn dotnet_namespace(api_plan: &PlannedSpec) -> String {
if !api_plan.module_path.is_root() {
return dotnet_module_namespace(&api_plan.module_path);
}
api_plan
.services
.first()
.and_then(|service| service.namespace.for_language(Language::Dotnet))
.map(ToOwned::to_owned)
.or_else(|| {
api_plan
.services
.first()
.map(|service| format!("Nexgen.{}", csharp_type_name(&service.name)))
})
.unwrap_or_else(|| "Nexgen.Generated".to_string())
}
fn dotnet_module_imports(api_plan: &PlannedSpec) -> Vec<String> {
api_plan
.data
.module_imports
.keys()
.map(dotnet_module_namespace)
.collect()
}
fn dotnet_module_namespace(module_path: &crate::spec::ModulePath) -> String {
let suffix = module_path
.0
.iter()
.map(|segment| csharp_type_name(&segment.to_upper_camel_case()))
.collect::<Vec<_>>()
.join(".");
if suffix.is_empty() {
"Nexgen.Generated".to_string()
} else {
format!("Nexgen.Generated.{suffix}")
}
}
fn dotnet_operations_class_name(service: &PlannedService) -> String {
service
.operations_class
.for_language(Language::Dotnet)
.map(csharp_type_name)
.unwrap_or_else(|| "Operations".to_string())
}
fn dotnet_endpoint_service_class_name(service: &PlannedService) -> String {
format!("{}Client", csharp_type_name(&service.name))
}
fn service_endpoint_constant_name(service: &PlannedService) -> String {
csharp_type_name(&format!("{}-endpoint", service.name))
}
pub(in crate::generator) fn field_property_name(field: &RecordFieldSpec<PlannedFamily>) -> String {
csharp_type_name(&field.name)
}
fn enum_value_name(name: &str) -> String {
name.trim_start_matches(|ch: char| ch == '_' || ch.is_ascii_digit())
.trim_start_matches("UNSPECIFIED")
.trim_start_matches('_')
.to_string()
}
pub(in crate::generator) fn csharp_type_name(name: &str) -> String {
csharp_ident(&name.to_upper_camel_case())
}
pub(in crate::generator) fn csharp_parameter_name(name: &str) -> String {
csharp_ident(&name.to_lower_camel_case())
}
fn csharp_ident(name: &str) -> String {
let candidate = if name.is_empty() {
"Value".to_string()
} else if name
.chars()
.next()
.is_some_and(|ch| ch == '_' || ch.is_ascii_alphabetic())
{
name.to_string()
} else {
format!("_{name}")
};
if CSHARP_KEYWORDS.contains(&candidate.as_str()) {
format!("@{candidate}")
} else {
candidate
}
}
pub(in crate::generator) fn csharp_string_literal(value: &str) -> String {
format!(
"\"{}\"",
value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
)
}
const CSHARP_KEYWORDS: &[&str] = &[
"abstract",
"as",
"base",
"bool",
"break",
"byte",
"case",
"catch",
"char",
"checked",
"class",
"const",
"continue",
"decimal",
"default",
"delegate",
"do",
"double",
"else",
"enum",
"event",
"explicit",
"extern",
"false",
"finally",
"fixed",
"float",
"for",
"foreach",
"goto",
"if",
"implicit",
"in",
"int",
"interface",
"internal",
"is",
"lock",
"long",
"namespace",
"new",
"null",
"object",
"operator",
"out",
"override",
"params",
"private",
"protected",
"public",
"readonly",
"ref",
"return",
"sbyte",
"sealed",
"short",
"sizeof",
"stackalloc",
"static",
"string",
"struct",
"switch",
"this",
"throw",
"true",
"try",
"typeof",
"uint",
"ulong",
"unchecked",
"unsafe",
"ushort",
"using",
"virtual",
"void",
"volatile",
"while",
];