Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions src/generators/go/http/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use super::{sigil_emit, sigil_emit_api};
use crate::codegen::traits::code_generator::CodeGenerator;
use crate::codegen::traits::file_writer::{FileInfo, FileWriter};
use crate::codegen::{GeneratorType, Language};
use crate::generators::request_inputs::plan_multipart_request_inputs;
use crate::ir::types::{IrInfo, IrSpec};

const DEFAULT_MODULE_PATH: &str = "example.com/sdk";
Expand Down Expand Up @@ -45,25 +46,27 @@ impl GoHttpCodeGenerator {
fn generate_ir(&self, ir: &IrSpec) -> Result<Vec<FileInfo>, Box<dyn Error + Send + Sync>> {
let module_path = self.module_path();
let header = render_file_header(&ir.info);
let request_inputs = plan_multipart_request_inputs(ir);

let mut files = Vec::new();

// Models
files.extend(
sigil_emit::generate_model_files(ir, &header).map_err(|msg| {
Box::<dyn Error + Send + Sync>::from(format!("sigil_emit models: {msg}"))
})?,
sigil_emit::generate_model_files(ir, &module_path, &header, &request_inputs).map_err(
|msg| Box::<dyn Error + Send + Sync>::from(format!("sigil_emit models: {msg}")),
)?,
);

// APIs
files.extend(
sigil_emit_api::generate_api_files(ir, &module_path, &header).map_err(|msg| {
Box::<dyn Error + Send + Sync>::from(format!("sigil_emit_api: {msg}"))
})?,
sigil_emit_api::generate_api_files(ir, &module_path, &header, &request_inputs)
.map_err(|msg| {
Box::<dyn Error + Send + Sync>::from(format!("sigil_emit_api: {msg}"))
})?,
);

// Runtime (hardcoded)
files.extend(runtime_files(&header));
files.extend(runtime_files(&header, request_inputs.has_uploads()));

// Project files: go.mod, README.md
files.push(go_mod_file(&module_path));
Expand Down
14 changes: 11 additions & 3 deletions src/generators/go/http/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,24 @@ use crate::codegen::traits::file_writer::FileInfo;
const CLIENT_GO: &str = include_str!("runtime/client.go.txt");
const AUTH_GO: &str = include_str!("runtime/auth.go.txt");
const ERRORS_GO: &str = include_str!("runtime/errors.go.txt");
const UPLOAD_FILE_GO: &str = include_str!("runtime/upload_file.go.txt");

/// Returns client.go, auth.go, errors.go ready to write.
///
/// The category routes these under `<output>/runtime/` via `FileWriter`.
pub fn runtime_files(header: &str) -> Vec<FileInfo> {
vec![
pub fn runtime_files(header: &str, include_upload_file: bool) -> Vec<FileInfo> {
let mut files = vec![
FileInfo::runtime("client.go".to_string(), with_header(header, CLIENT_GO)),
FileInfo::runtime("auth.go".to_string(), with_header(header, AUTH_GO)),
FileInfo::runtime("errors.go".to_string(), with_header(header, ERRORS_GO)),
]
];
if include_upload_file {
files.push(FileInfo::runtime(
"upload_file.go".to_string(),
with_header(header, UPLOAD_FILE_GO),
));
}
files
}

fn with_header(header: &str, body: &str) -> String {
Expand Down
21 changes: 21 additions & 0 deletions src/generators/go/http/runtime/upload_file.go.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package runtime

type UploadFile struct {
Data []byte
Filename string
}

func NewUploadFile(data []byte, filename string) UploadFile {
return UploadFile{Data: data, Filename: filename}
}

func NewUploadFileBytes(data []byte) UploadFile {
return UploadFile{Data: data}
}

func (f UploadFile) FilenameOrDefault(defaultName string) string {
if f.Filename != "" {
return f.Filename
}
return defaultName
}
55 changes: 54 additions & 1 deletion src/generators/go/http/sigil_emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
//! name directly.

use crate::codegen::traits::file_writer::FileInfo;
use crate::generators::request_inputs::{
RequestInputField, RequestInputFieldKind, RequestInputModel, RequestInputPlan,
};
use crate::ir::types::{
IrEnum, IrEnumValueType, IrIntersection, IrObject, IrPrimitive, IrProperty, IrSchema,
IrSchemaKind, IrSpec, IrTaggedUnion, IrTypeExpr, IrUnion, TaggingStyle,
Expand All @@ -36,7 +39,12 @@ const RENDER_WIDTH: usize = 100;

/// Generate every model file from the IR. Each emitted file carries the
/// passed-in header (e.g., the `// Code generated` banner).
pub fn generate_model_files(ir: &IrSpec, header: &str) -> Result<Vec<FileInfo>, String> {
pub fn generate_model_files(
ir: &IrSpec,
module_path: &str,
header: &str,
request_inputs: &RequestInputPlan,
) -> Result<Vec<FileInfo>, String> {
let mut files = Vec::new();
for (name, schema) in &ir.schemas {
let Some(body) = emit_model_body(schema) else {
Expand All @@ -47,6 +55,9 @@ pub fn generate_model_files(ir: &IrSpec, header: &str) -> Result<Vec<FileInfo>,
};
files.push(model_file(&schema.name, header, &body));
}
for model in request_inputs.models() {
files.push(request_input_model_file(model, module_path, header));
}
Ok(files)
}

Expand All @@ -66,6 +77,48 @@ fn model_file(name: &str, header: &str, body: &str) -> FileInfo {
FileInfo::model(filename, content)
}

fn request_input_model_file(
model: &RequestInputModel,
module_path: &str,
header: &str,
) -> FileInfo {
let name = model.name.to_pascal_case();
let stem = model.name.to_snake_case();
let filename = if stem.ends_with("_test") {
format!("{stem}_model.go")
} else {
format!("{stem}.go")
};
let needs_runtime = model.fields.iter().any(RequestInputField::is_upload);
let mut body = String::new();
body.push_str(&format!("package {MODELS_PACKAGE}\n\n"));
if needs_runtime {
body.push_str(&format!("import \"{module_path}/runtime\"\n\n"));
}
body.push_str(&format!("type {name} struct {{\n"));
for field in &model.fields {
let field_name = go_field_name(&field.wire_name);
let mut field_type = request_input_go_type(field);
if !field.required && !field_type.starts_with('*') {
field_type = format!("*{field_type}");
}
body.push_str(&format!("\t{field_name} {field_type}\n"));
}
body.push_str("}\n");

let mut content = String::with_capacity(header.len() + body.len());
content.push_str(header);
content.push_str(&body);
FileInfo::model(filename, content)
}

fn request_input_go_type(field: &RequestInputField) -> String {
match field.kind {
RequestInputFieldKind::UploadFile { .. } => "runtime.UploadFile".to_string(),
RequestInputFieldKind::SchemaValue => go_type_str(&field.type_expr),
}
}

/// Dispatch on schema kind. Returns the rendered file body (package + imports
/// + declarations) but not the pre-package header comment.
fn emit_model_body(schema: &IrSchema) -> Option<String> {
Expand Down
77 changes: 54 additions & 23 deletions src/generators/go/http/sigil_emit_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use std::collections::{BTreeMap, BTreeSet, HashSet};

use crate::codegen::traits::file_writer::FileInfo;
use crate::generators::multipart::{MultipartValueEncoding, multipart_parts_for_request_body};
use crate::generators::request_inputs::{RequestInputPlan, request_input_for_operation};
use crate::ir::types::{
IrOperation, IrParameter, IrPrimitive, IrRequestBody, IrResponse, IrSpec, IrTypeExpr,
ParameterLocation,
Expand All @@ -44,6 +45,7 @@ pub fn generate_api_files(
ir: &IrSpec,
module_path: &str,
header: &str,
request_inputs: &RequestInputPlan,
) -> Result<Vec<FileInfo>, String> {
let by_tag = group_by_tag(&ir.operations);
let mut files = Vec::with_capacity(by_tag.len());
Expand All @@ -55,7 +57,7 @@ pub fn generate_api_files(
} else {
format!("{stem}.go")
};
let body = emit_api_file(tag, ops, ir, module_path);
let body = emit_api_file(tag, ops, ir, module_path, request_inputs);
let content = format!("{header}{body}");
files.push(FileInfo::api(filename, content));
}
Expand All @@ -81,11 +83,20 @@ fn group_by_tag(operations: &[IrOperation]) -> BTreeMap<String, Vec<&IrOperation
// File assembly
// ---------------------------------------------------------------------------

fn emit_api_file(tag: &str, ops: &[&IrOperation], ir: &IrSpec, module_path: &str) -> String {
fn emit_api_file(
tag: &str,
ops: &[&IrOperation],
ir: &IrSpec,
module_path: &str,
request_inputs: &RequestInputPlan,
) -> String {
let struct_name = format!("{}API", tag.to_pascal_case());

// Pre-plan each operation so we can build specs from the plans.
let plans: Vec<OpPlan> = ops.iter().map(|op| plan_operation(op, ir)).collect();
let plans: Vec<OpPlan> = ops
.iter()
.map(|op| plan_operation(op, ir, request_inputs))
.collect();

let filename = format!("{}.go", tag.to_snake_case());
let mut fb = FileSpec::builder(&filename)
Expand Down Expand Up @@ -141,6 +152,9 @@ fn collect_body_imports(plans: &[OpPlan<'_>], module_path: &str) -> Vec<ImportSp
pkgs.insert("fmt".to_string());
if let Some(parts) = &body.multipart_parts {
pkgs.insert("bytes".to_string());
if parts.iter().any(|part| part.is_binary) {
pkgs.insert("mime".to_string());
}
pkgs.insert("mime/multipart".to_string());
pkgs.insert("net/textproto".to_string());
for part in parts {
Expand Down Expand Up @@ -485,7 +499,11 @@ struct TypedResponse {
decoding: ResponseDecoding,
}

fn plan_operation<'a>(op: &'a IrOperation, ir: &IrSpec) -> OpPlan<'a> {
fn plan_operation<'a>(
op: &'a IrOperation,
ir: &IrSpec,
request_inputs: &RequestInputPlan,
) -> OpPlan<'a> {
let op_id = sanitize_operation_id(&op.operation_id, &op.method, &op.path);
let method_name = op_id.to_pascal_case();
let response_type = format!("{method_name}Response");
Expand Down Expand Up @@ -517,7 +535,7 @@ fn plan_operation<'a>(op: &'a IrOperation, ir: &IrSpec) -> OpPlan<'a> {
let body = op
.request_body
.as_ref()
.and_then(|b| plan_body(b, ir, &mut used_names));
.and_then(|b| plan_body(op, b, ir, request_inputs, &mut used_names));

let typed_responses = op.responses.iter().filter_map(plan_response).collect();

Expand All @@ -534,15 +552,20 @@ fn plan_operation<'a>(op: &'a IrOperation, ir: &IrSpec) -> OpPlan<'a> {
}

fn plan_body(
op: &IrOperation,
b: &IrRequestBody,
ir: &IrSpec,
request_inputs: &RequestInputPlan,
used_names: &mut HashSet<String>,
) -> Option<BodyBinding> {
let (media_type, t) = pick_body_content(b)?;
let encoding = body_encoding(&media_type);
let base_ty = match encoding {
BodyEncoding::TextPlain => "string".to_string(),
BodyEncoding::OctetStream => "[]byte".to_string(),
BodyEncoding::Multipart => request_input_for_operation(request_inputs, op, &media_type)
.map(|input| format!("models.{}", input.name.to_pascal_case()))
.unwrap_or_else(|| go_type_str(&t)),
_ => go_type_str(&t),
};
let go_type =
Expand Down Expand Up @@ -876,7 +899,9 @@ fn emit_multipart_body(
emit_required_multipart_part(cb, part, &value_expr);
} else {
cb.begin_control_flow(&format!("if {value_expr} != nil"), ());
emit_required_multipart_part(cb, part, &format!("*{value_expr}"));
cb.add(&format!("value := *{value_expr}"), ());
cb.add_line();
emit_required_multipart_part(cb, part, "value");
cb.end_control_flow();
}
}
Expand All @@ -903,23 +928,29 @@ fn emit_required_multipart_part(
cb.add_line();
cb.add("partHeader := textproto.MIMEHeader{}", ());
cb.add_line();
let disposition = if part.is_binary {
format!(
"form-data; name={}; filename={}",
go_string_literal(&part.wire_name),
go_string_literal(&part.wire_name)
)
if part.is_binary {
cb.add(
&format!(
"disposition := mime.FormatMediaType(\"form-data\", map[string]string{{\"name\": {}, \"filename\": {value_expr}.FilenameOrDefault({})}})",
go_string_literal(&part.wire_name),
go_string_literal(&part.wire_name)
),
(),
);
cb.add_line();
cb.add("partHeader.Set(\"Content-Disposition\", disposition)", ());
cb.add_line();
} else {
format!("form-data; name={}", go_string_literal(&part.wire_name))
};
cb.add(
&format!(
"partHeader.Set(\"Content-Disposition\", {})",
go_string_literal(&disposition)
),
(),
);
cb.add_line();
let disposition = format!("form-data; name={}", go_string_literal(&part.wire_name));
cb.add(
&format!(
"partHeader.Set(\"Content-Disposition\", {})",
go_string_literal(&disposition)
),
(),
);
cb.add_line();
}
cb.add(
&format!(
"partHeader.Set(\"Content-Type\", {})",
Expand All @@ -939,7 +970,7 @@ fn emit_required_multipart_part(
cb.end_control_flow();
if part.is_binary {
cb.begin_control_flow(
&format!("if _, err := partWriter.Write({value_expr}); err != nil"),
&format!("if _, err := partWriter.Write({value_expr}.Data); err != nil"),
(),
);
cb.add(
Expand Down
21 changes: 14 additions & 7 deletions src/generators/java/okhttp/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use super::{sigil_emit, sigil_emit_api};
use crate::codegen::traits::code_generator::CodeGenerator;
use crate::codegen::traits::file_writer::{FileInfo, FileWriter};
use crate::codegen::{GeneratorType, Language};
use crate::generators::request_inputs::plan_multipart_request_inputs;
use crate::ir::types::{IrInfo, IrSpec};

const DEFAULT_PACKAGE: &str = "com.example.sdk";
Expand All @@ -34,22 +35,28 @@ impl JavaOkhttpCodeGenerator {
fn generate_ir(&self, ir: &IrSpec) -> Result<Vec<FileInfo>, Box<dyn Error + Send + Sync>> {
let package_name = self.package_name();
let header = render_file_header(&ir.info);
let request_inputs = plan_multipart_request_inputs(ir);

let mut files = Vec::new();

files.extend(
sigil_emit::generate_model_files(ir, &package_name, &header).map_err(|msg| {
Box::<dyn Error + Send + Sync>::from(format!("sigil_emit models: {msg}"))
})?,
sigil_emit::generate_model_files(ir, &package_name, &header, &request_inputs).map_err(
|msg| Box::<dyn Error + Send + Sync>::from(format!("sigil_emit models: {msg}")),
)?,
);

files.extend(
sigil_emit_api::generate_api_files(ir, &package_name, &header).map_err(|msg| {
Box::<dyn Error + Send + Sync>::from(format!("sigil_emit_api: {msg}"))
})?,
sigil_emit_api::generate_api_files(ir, &package_name, &header, &request_inputs)
.map_err(|msg| {
Box::<dyn Error + Send + Sync>::from(format!("sigil_emit_api: {msg}"))
})?,
);

files.extend(runtime_files(&header, &package_name));
files.extend(runtime_files(
&header,
&package_name,
request_inputs.has_uploads(),
));

files.push(build_gradle_file(&package_name, &ir.info));
files.push(readme_file(&ir.info));
Expand Down
Loading
Loading