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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ The `generate` command reads a JSON or YAML OpenAPI/Swagger document from a loca
| `-o`, `--output <path>` | Output file for the generated Zig code. Defaults to `generated.zig`. Parent directories are created when needed. |
| `--base-url <url>` | Base URL baked into the generated `Client`. Defaults to the server URL from the OpenAPI/Swagger document. |
| `--resource-wrappers <mode>` | Generate resource wrapper namespaces. Modes: `none`, `tags`, `paths`, `hybrid`. Defaults to `paths`. |
| `--sse-buffer <mode>` | SSE parse buffer size: `small` (8KB line / 64KB event) or `large` (256KB line / 1MB event). Defaults to `small`. |


### Examples

Expand Down Expand Up @@ -406,7 +406,7 @@ Generated files are self-contained Zig source files. The current unified generat
- `operationResult(...) !ApiResult(T)` for parsed success plus preserved API/parse-error bodies.
- Generic helpers such as `requestRaw`, `getRaw`, `postJsonRaw`, `getJsonResult`, and `postJsonResult`.
- Query parameter helpers that percent-encode names and string values with `std.Uri.Component.percentEncode`; optional query parameters are nullable.
- Bounded SSE parsing helpers: `parseSseBytes`, `parseSseReader`, `parseSseBytesTyped`, and `parseSseReaderTyped`. SSE buffer size defaults to `small` (8KB line / 64KB event) and can be switched to `large` (256KB line / 1MB event) with `--sse-buffer large`. Stream helpers are generated for every POST operation whose response declares `text/event-stream` content — the function name is `{operationId}Streaming` (with an `Events` variant for typed JSON events).
- Bounded SSE parsing helpers: `parseSseBytes`, `parseSseReader`, `parseSseBytesTyped`, and `parseSseReaderTyped`. SSE buffer size is fixed at 256KB for lines and 1MB for events. Stream helpers are generated for every POST operation whose response declares `text/event-stream` content — the function name is `{operationId}Streaming` (with an `Events` variant for typed JSON events).
- Resource wrapper namespaces by default, for example `pet.get(...)` and `store.order.get(...)`, derived from paths unless `--resource-wrappers` changes the mode. Wrapper names are sanitized generated conveniences, not hand-designed SDK names.

Parsed JSON responses use `.ignore_unknown_fields = true` so compatible providers can add response fields without breaking callers. Ambiguous or intentionally open-ended schemas use `std.json.Value`; see [`docs/json-value-typing-policy.md`](docs/json-value-typing-policy.md) for the current policy. For OpenAPI 3.1, the converter has stronger composite-schema handling for object/ref `allOf`, preserved `oneOf`/`anyOf` metadata, and nullable type arrays; do not assume every converter has identical composite support.
Expand Down Expand Up @@ -617,3 +617,5 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
## Support

If you encounter any issues or have questions, please [open an issue](https://github.com/christianhelle/openapi2zig/issues) on GitHub.


10 changes: 5 additions & 5 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -424,10 +424,7 @@ <h3>Options</h3>
<td><code>--resource-wrappers &lt;mode&gt;</code></td>
<td>Resource wrapper mode: <code>none</code>, <code>tags</code>, <code>paths</code>, or <code>hybrid</code>. Defaults to <code>paths</code>.</td>
</tr>
<tr>
<td><code>--sse-buffer &lt;mode&gt;</code></td>
<td>SSE parse buffer size: <code>small</code> (8KB line / 64KB event) or <code>large</code> (256KB line / 1MB event). Defaults to <code>small</code>.</td>
</tr>

</tbody>
</table>

Expand All @@ -448,7 +445,7 @@ <h3>Current Capabilities</h3>
<li>✅ Generated <code>Client</code> with base URL, API key, and borrowed headers</li>
<li>✅ Percent-encoded query parameters and loose response parsing</li>
<li>✅ Raw/result helpers that preserve response bodies</li>
<li>✅ Bounded raw and typed SSE parser helpers (configurable buffer size)</li>
<li>✅ Bounded raw and typed SSE parser helpers (large buffer size, 256KB line / 1MB event)</li>
<li>✅ Resource wrapper namespaces generated from paths, tags, or hybrid metadata</li>
</ul>
</div>
Expand Down Expand Up @@ -798,3 +795,6 @@ <h3>Additional Links</h3>
<script src="./script.js"></script>
</body>
</html>



57 changes: 15 additions & 42 deletions src/cli.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,13 @@ pub const ResourceWrapperMode = enum {
hybrid,
};

pub const SseBufferMode = enum {
small,
large,

pub fn maxLineSize(self: SseBufferMode) usize {
return switch (self) {
.small => 8 * 1024,
.large => 256 * 1024,
};
}

pub fn maxEventSize(self: SseBufferMode) usize {
return switch (self) {
.small => 64 * 1024,
.large => 1024 * 1024,
};
}
};

pub const CliArgs = struct {
input_path: []const u8,
output_path: ?[]const u8 = null,
base_url: ?[]const u8 = null,
resource_wrappers: ResourceWrapperMode = .paths,
models_only: bool = false,
multiple_files: bool = false,
sse_buffer: SseBufferMode = .small,
};

pub const ParsedArgs = struct {
Expand Down Expand Up @@ -65,7 +45,6 @@ pub fn parse(args: []const [:0]const u8) !ParsedArgs {
var resource_wrappers: ResourceWrapperMode = .paths;
var models_only = false;
var multiple_files = false;
var sse_buffer: SseBufferMode = .small;

var i: usize = 2;
while (i < args.len) : (i += 1) {
Expand Down Expand Up @@ -111,18 +90,6 @@ pub fn parse(args: []const [:0]const u8) !ParsedArgs {
models_only = true;
} else if (std.mem.eql(u8, arg, "--multiple-files")) {
multiple_files = true;
} else if (std.mem.eql(u8, arg, "--sse-buffer")) {
i += 1;
if (i >= args.len) {
printUsage();
std.debug.print("\nError: SSE buffer mode required\n", .{});
return error.InvalidArguments;
}
sse_buffer = parseSseBufferMode(args[i]) orelse {
printUsage();
std.debug.print("\nError: invalid SSE buffer mode '{s}'\n", .{args[i]});
return error.InvalidArguments;
};
}
}

Expand All @@ -140,7 +107,6 @@ pub fn parse(args: []const [:0]const u8) !ParsedArgs {
.resource_wrappers = resource_wrappers,
.models_only = models_only,
.multiple_files = multiple_files,
.sse_buffer = sse_buffer,
},
};
}
Expand All @@ -153,12 +119,6 @@ fn parseResourceWrapperMode(value: []const u8) ?ResourceWrapperMode {
return null;
}

fn parseSseBufferMode(value: []const u8) ?SseBufferMode {
if (std.mem.eql(u8, value, "small")) return .small;
if (std.mem.eql(u8, value, "large")) return .large;
return null;
}

fn printUsage() void {
std.debug.print(
\\
Expand All @@ -181,8 +141,6 @@ fn printUsage() void {
\\ --models-only Generate only Zig models, skipping the API client.
\\ --multiple-files Generate separate output files for models, runtime, and API client
\\ into the output directory specified by -o.
\\ --sse-buffer <mode> SSE parse buffer size: small (8KB line / 64KB event)
\\ or large (256KB line / 1MB event). (default: small)
\\
\\ EXAMPLES:
\\ openapi2zig generate -i ./openapi/petstore.json -o api.zig
Expand Down Expand Up @@ -245,3 +203,18 @@ test "parse generate supports multiple-files flag" {
try std.testing.expect(parsed.args.multiple_files);
try std.testing.expectEqualStrings("openapi.json", parsed.args.input_path);
}

test "parse generate silently ignores --sse-buffer flag" {
const argv = [_][:0]const u8{
"openapi2zig",
"generate",
"-i",
"openapi.json",
"--sse-buffer",
"large",
};

const parsed = try parse(&argv);

try std.testing.expectEqualStrings("openapi.json", parsed.args.input_path);
}
3 changes: 2 additions & 1 deletion src/generator.zig
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ fn generateMultipleFiles(allocator: std.mem.Allocator, io: std.Io, cwd: std.Io.D

if (args.models_only) return;

var runtime_gen = RuntimeGenerator.init(allocator, args.sse_buffer);
var runtime_gen = RuntimeGenerator.init(allocator);
defer runtime_gen.deinit();
const generated_runtime = try runtime_gen.generate();
defer allocator.free(generated_runtime);
Expand Down Expand Up @@ -227,3 +227,4 @@ test "unsupported OpenAPI versions return a distinct generator error" {
}),
);
}

17 changes: 7 additions & 10 deletions src/generators/unified/api_generator.zig
Original file line number Diff line number Diff line change
Expand Up @@ -672,15 +672,7 @@ pub const UnifiedApiGenerator = struct {
}

fn generateSseBufferConstants(self: *UnifiedApiGenerator) !void {
try self.buffer.appendSlice(self.allocator, "const max_sse_line_size = ");
const line_size = try std.fmt.allocPrint(self.allocator, "{d}", .{self.args.sse_buffer.maxLineSize()});
defer self.allocator.free(line_size);
try self.buffer.appendSlice(self.allocator, line_size);
try self.buffer.appendSlice(self.allocator, ";\nconst max_sse_event_size = ");
const event_size = try std.fmt.allocPrint(self.allocator, "{d}", .{self.args.sse_buffer.maxEventSize()});
defer self.allocator.free(event_size);
try self.buffer.appendSlice(self.allocator, event_size);
try self.buffer.appendSlice(self.allocator, ";\n\n");
try self.buffer.appendSlice(self.allocator, "const max_sse_line_size = 256 * 1024;\nconst max_sse_event_size = 1024 * 1024;\n\n");
}

fn generateHttpObserverType(self: *UnifiedApiGenerator) !void {
Expand Down Expand Up @@ -1333,7 +1325,9 @@ pub const UnifiedApiGenerator = struct {
try self.buffer.appendSlice(self.allocator, "std.json.Value");
}
} else {
if (param.location == .query and !param.required) try self.buffer.appendSlice(self.allocator, "?");
if (param.location == .query and !param.required) {
try self.buffer.appendSlice(self.allocator, "?");
}
if (param.schema) |schema| {
try self.appendZigQueryTypeFromSchema(schema);
} else if (param.type) |param_type| {
Expand Down Expand Up @@ -1758,12 +1752,14 @@ pub const UnifiedApiGenerator = struct {
try self.buffer.appendSlice(self.allocator, " // TODO(#53-followup): multipart/form-data and x-www-form-urlencoded request bodies are not yet supported; falling back to JSON encoding.\n");
try self.buffer.appendSlice(self.allocator, "\n var str: std.Io.Writer.Allocating = .init(allocator);\n");
try self.buffer.appendSlice(self.allocator, " defer str.deinit();\n\n");

try self.buffer.appendSlice(self.allocator, " try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer);\n");
try self.buffer.appendSlice(self.allocator, " const payload = str.written();\n");
},
else => {
try self.buffer.appendSlice(self.allocator, "\n var str: std.Io.Writer.Allocating = .init(allocator);\n");
try self.buffer.appendSlice(self.allocator, " defer str.deinit();\n\n");

try self.buffer.appendSlice(self.allocator, " try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer);\n");
try self.buffer.appendSlice(self.allocator, " const payload = str.written();\n");
},
Expand Down Expand Up @@ -1962,3 +1958,4 @@ test "BodyKind :: classifyBody routes media types correctly" {
try t.expectEqual(BodyKind.text, classifyBody("text/plain; charset=utf-8"));
try t.expectEqual(BodyKind.form, classifyBody("multipart/form-data; boundary=abc"));
}

16 changes: 3 additions & 13 deletions src/generators/unified/runtime_generator.zig
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
const std = @import("std");
const cli = @import("../../cli.zig");

pub const RuntimeGenerator = struct {
allocator: std.mem.Allocator,
buffer: std.ArrayList(u8),
sse_buffer: cli.SseBufferMode,

pub fn init(allocator: std.mem.Allocator, sse_buffer: cli.SseBufferMode) RuntimeGenerator {
pub fn init(allocator: std.mem.Allocator) RuntimeGenerator {
return RuntimeGenerator{
.allocator = allocator,
.buffer = std.ArrayList(u8).empty,
.sse_buffer = sse_buffer,
};
}

Expand Down Expand Up @@ -97,15 +94,7 @@ pub const RuntimeGenerator = struct {
}

fn generateSseBufferConstants(self: *RuntimeGenerator) !void {
try self.buffer.appendSlice(self.allocator, "\nconst max_sse_line_size = ");
const line_size = try std.fmt.allocPrint(self.allocator, "{d}", .{self.sse_buffer.maxLineSize()});
defer self.allocator.free(line_size);
try self.buffer.appendSlice(self.allocator, line_size);
try self.buffer.appendSlice(self.allocator, ";\nconst max_sse_event_size = ");
const event_size = try std.fmt.allocPrint(self.allocator, "{d}", .{self.sse_buffer.maxEventSize()});
defer self.allocator.free(event_size);
try self.buffer.appendSlice(self.allocator, event_size);
try self.buffer.appendSlice(self.allocator, ";\n\n");
try self.buffer.appendSlice(self.allocator, "\nconst max_sse_line_size = 256 * 1024;\nconst max_sse_event_size = 1024 * 1024;\n\n");
}

fn generateSseFunctions(self: *RuntimeGenerator) !void {
Expand Down Expand Up @@ -229,3 +218,4 @@ pub const RuntimeGenerator = struct {
);
}
};

3 changes: 2 additions & 1 deletion src/lib.zig
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ pub fn generateCodeMultiple(allocator: std.mem.Allocator, io: std.Io, unified_do
return .{ .models = models_with_header };
}

var runtime_gen = RuntimeGenerator.init(allocator, args.sse_buffer);
var runtime_gen = RuntimeGenerator.init(allocator);
defer runtime_gen.deinit();
const runtime_code = try runtime_gen.generate();
defer allocator.free(runtime_code);
Expand Down Expand Up @@ -357,3 +357,4 @@ test {
std.testing.refAllDecls(@This());
_ = @import("tests.zig");
}

Loading