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
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This tool supports the following OpenAPI and Swagger specifications:
- **OpenAPI v3.1** - Full support
- **OpenAPI v3.2** - Full support

All specifications are supported in JSON format. YAML support may be added in future releases.
All specifications are supported in JSON and YAML format.

## Features

Expand Down Expand Up @@ -201,7 +201,7 @@ The `generate` command reads a JSON OpenAPI/Swagger document from a local file o

| Flag | Description |
| :--- | :--- |
| `-i`, `--input <PATH_OR_URL>` | OpenAPI/Swagger JSON spec from a file path or `http`/`https` URL. Required. |
| `-i`, `--input <PATH_OR_URL>` | OpenAPI/Swagger JSON or YAML spec from a file path or `http`/`https` URL. Required. |
| `-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`. |
Expand All @@ -213,6 +213,11 @@ The `generate` command reads a JSON OpenAPI/Swagger document from a local file o
openapi2zig generate -i openapi/v3.0/petstore.json -o api.zig
```

**From a local YAML file:**
```bash
openapi2zig generate -i openapi/v3.0/petstore.yaml -o api.zig
```

**From a remote URL:**
```bash
openapi2zig generate -i https://petstore3.swagger.io/api/v3/openapi.json -o api.zig
Expand Down Expand Up @@ -327,16 +332,21 @@ pub fn main(init: std.process.Init) !void {

#### Version Detection

- `detectVersion(allocator, json_content)` - Detect OpenAPI/Swagger version
- `detectVersion(allocator, json_content)` - Detect OpenAPI/Swagger version from JSON
- `detectVersionFromYaml(allocator, yaml_content)` - Detect OpenAPI/Swagger version from YAML
- `ApiVersion` - Enum representing supported API versions (.v2_0, .v3_0, .v3_1, .v3_2, .Unsupported)

#### Parsing Functions

- `parseToUnified(allocator, json_content)` - Parse any supported version (v2.0, v3.0, v3.1, v3.2) to unified representation
- `parseToUnified(allocator, json_content)` - Parse any supported JSON version (v2.0, v3.0, v3.1, v3.2) to unified representation
- `parseOpenApi(allocator, json_content)` - Parse OpenAPI v3.0 specifically
- `parseOpenApiYaml(allocator, yaml_content)` - Parse OpenAPI v3.0 YAML specifically
- `parseOpenApi31(allocator, json_content)` - Parse OpenAPI v3.1 specifically
- `parseOpenApi31Yaml(allocator, yaml_content)` - Parse OpenAPI v3.1 YAML specifically
- `parseOpenApi32(allocator, json_content)` - Parse OpenAPI v3.2 specifically
- `parseOpenApi32Yaml(allocator, yaml_content)` - Parse OpenAPI v3.2 YAML specifically
- `parseSwagger(allocator, json_content)` - Parse Swagger v2.0 specifically
- `parseSwaggerYaml(allocator, yaml_content)` - Parse Swagger v2.0 YAML specifically

#### Code Generation

Expand Down
10 changes: 10 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ pub fn build(b: *std.Build) void {
const run_integration_tests = b.option(bool, "run-integration", "Run network integration tests") orelse false;
const build_info = createBuildInfoOptions(b, run_integration_tests);
const package_snapshot_step = createPackageSnapshotStep(b);
const yaml_dep = b.dependency("yaml", .{
.target = target,
.optimize = optimize,
});

// Library module for external packages
const openapi2zig_mod = b.addModule("openapi2zig", .{
Expand All @@ -15,6 +19,7 @@ pub fn build(b: *std.Build) void {
});
openapi2zig_mod.addIncludePath(b.path("src"));
openapi2zig_mod.addOptions("build_info", build_info);
openapi2zig_mod.addImport("yaml", yaml_dep.module("yaml"));

// CLI executable
const exe_root_module = b.createModule(.{
Expand All @@ -23,6 +28,7 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
});
exe_root_module.addOptions("build_info", build_info);
exe_root_module.addImport("yaml", yaml_dep.module("yaml"));
const exe = b.addExecutable(.{
.name = "openapi2zig",
.root_module = exe_root_module,
Expand All @@ -37,6 +43,7 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
});
lib_root_module.addOptions("build_info", build_info);
lib_root_module.addImport("yaml", yaml_dep.module("yaml"));
const lib = b.addLibrary(.{
.name = "openapi2zig",
.root_module = lib_root_module,
Expand Down Expand Up @@ -115,6 +122,7 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
});
tests_mod.addOptions("build_info", build_info);
tests_mod.addImport("yaml", yaml_dep.module("yaml"));

const exe_unit_tests = b.addTest(.{
.root_module = tests_mod,
Expand Down Expand Up @@ -246,7 +254,9 @@ fn getPackageSnapshotFiles(allocator: std.mem.Allocator, io: std.Io) ?[]const u8
"build.zig",
"build.zig.zon",
"src",
"openapi",
"generated",
"vendor/zig-yaml",
"LICENSE",
"README.md",
"examples/package_consumer",
Expand Down
14 changes: 5 additions & 9 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
// internet connectivity.
.dependencies = .{
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
.yaml = .{
.path = "vendor/zig-yaml",
},
//.example = .{
// // When updating this field to a new URL, be sure to delete the corresponding
// // `hash`, otherwise you are communicating that you expect to find the old hash at
Expand Down Expand Up @@ -66,20 +69,13 @@
// .lazy = false,
//},
},

// Specifies the set of files and directories that are included in this package.
// Only files and directories listed here are included in the `hash` that
// is computed for this package. Only files listed here will remain on disk
// when using the zig package manager. As a rule of thumb, one should list
// files required for compilation plus any license(s).
// Paths are relative to the build root. Use the empty string (`""`) to refer to
// the build root itself.
// A directory listed here means that all files within, recursively, are included.
.paths = .{
"build.zig",
"build.zig.zon",
"src",
"openapi",
"generated",
"vendor/zig-yaml",
"LICENSE",
"README.md",
},
Expand Down
91 changes: 51 additions & 40 deletions src/generator.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const cli = @import("cli.zig");
const detector = @import("detector.zig");
const models = @import("models.zig");
const input_loader = @import("input_loader.zig");
const yaml_loader = @import("yaml_loader.zig");
const OpenApiConverter = @import("generators/converters/openapi_converter.zig").OpenApiConverter;
const OpenApi31Converter = @import("generators/converters/openapi31_converter.zig").OpenApi31Converter;
const OpenApi32Converter = @import("generators/converters/openapi32_converter.zig").OpenApi32Converter;
Expand Down Expand Up @@ -47,49 +48,59 @@ pub fn generateCode(allocator: std.mem.Allocator, io: std.Io, args: cli.CliArgs)
const file_contents = try input_loader.loadInput(allocator, io, source);
defer allocator.free(file_contents);

switch (extension) {
.YAML => {
std.debug.print("YAML support is not yet implemented\n", .{});
return GeneratorErrors.UnsupportedExtension;
},
.JSON => {
const version = detector.getOpenApiVersion(allocator, file_contents) catch |err| {
std.debug.print("Failed to parse OpenAPI version: {}\n", .{err});
var normalized_yaml_json: ?[]const u8 = null;
defer if (normalized_yaml_json) |json_contents| allocator.free(json_contents);

const json_contents = switch (extension) {
.YAML => blk: {
normalized_yaml_json = yaml_loader.yamlToJson(allocator, file_contents) catch |err| {
std.debug.print("Failed to parse YAML OpenAPI document: {}\n", .{err});
return err;
};
break :blk normalized_yaml_json.?;
},
.JSON => file_contents,
};

try generateCodeFromJsonContents(allocator, io, json_contents, args);
}

fn generateCodeFromJsonContents(allocator: std.mem.Allocator, io: std.Io, json_contents: []const u8, args: cli.CliArgs) !void {
const version = detector.getOpenApiVersion(allocator, json_contents) catch |err| {
std.debug.print("Failed to parse OpenAPI version: {}\n", .{err});
return err;
};

std.debug.print("Detected OpenAPI version: {s}\n", .{detector.getOpenApiVersionString(version)});

switch (version) {
.v2_0 => {
var swagger = try models.SwaggerDocument.parseFromJson(allocator, file_contents);
defer swagger.deinit(allocator);
std.debug.print("Successfully parsed Swagger v2.0 document\n", .{});
try generateCodeFromSwaggerDocument(allocator, io, swagger, args);
},
.v3_0 => {
var openapi = try models.OpenApiDocument.parseFromJson(allocator, file_contents);
defer openapi.deinit(allocator);
std.debug.print("Successfully parsed OpenAPI v3.0 document\n", .{});
try generateCodeFromOpenApiDocument(allocator, io, openapi, args);
},
.v3_1 => {
var openapi31 = try models.OpenApi31Document.parseFromJson(allocator, file_contents);
defer openapi31.deinit(allocator);
std.debug.print("Successfully parsed OpenAPI v3.1 document\n", .{});
try generateCodeFromOpenApi31Document(allocator, io, openapi31, args);
},
.v3_2 => {
var openapi32 = try models.OpenApi32Document.parseFromJson(allocator, file_contents);
defer openapi32.deinit(allocator);
std.debug.print("Successfully parsed OpenAPI v3.2 document\n", .{});
try generateCodeFromOpenApi32Document(allocator, io, openapi32, args);
},
else => {
std.debug.print("Unsupported OpenAPI version: {s}\n", .{detector.getOpenApiVersionString(version)});
return GeneratorErrors.UnsupportedExtension;
},
}
std.debug.print("Detected OpenAPI version: {s}\n", .{detector.getOpenApiVersionString(version)});

switch (version) {
.v2_0 => {
var swagger = try models.SwaggerDocument.parseFromJson(allocator, json_contents);
defer swagger.deinit(allocator);
std.debug.print("Successfully parsed Swagger v2.0 document\n", .{});
try generateCodeFromSwaggerDocument(allocator, io, swagger, args);
},
.v3_0 => {
var openapi = try models.OpenApiDocument.parseFromJson(allocator, json_contents);
defer openapi.deinit(allocator);
std.debug.print("Successfully parsed OpenAPI v3.0 document\n", .{});
try generateCodeFromOpenApiDocument(allocator, io, openapi, args);
},
.v3_1 => {
var openapi31 = try models.OpenApi31Document.parseFromJson(allocator, json_contents);
defer openapi31.deinit(allocator);
std.debug.print("Successfully parsed OpenAPI v3.1 document\n", .{});
try generateCodeFromOpenApi31Document(allocator, io, openapi31, args);
},
.v3_2 => {
var openapi32 = try models.OpenApi32Document.parseFromJson(allocator, json_contents);
defer openapi32.deinit(allocator);
std.debug.print("Successfully parsed OpenAPI v3.2 document\n", .{});
try generateCodeFromOpenApi32Document(allocator, io, openapi32, args);
},
else => {
std.debug.print("Unsupported OpenAPI version: {s}\n", .{detector.getOpenApiVersionString(version)});
return GeneratorErrors.UnsupportedExtension;

Copilot AI Apr 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CLI returns UnsupportedExtension when the OpenAPI version is unsupported. This error type/message is misleading (the extension may be valid) and makes it harder for callers to distinguish “unsupported spec version” from “unsupported file type”. Consider introducing a dedicated UnsupportedVersion error (and/or updating the printed message) for this branch.

Suggested change
return GeneratorErrors.UnsupportedExtension;
return error.UnsupportedVersion;

Copilot uses AI. Check for mistakes.
},
}
}
Expand Down
41 changes: 41 additions & 0 deletions src/lib.zig
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
//! ```

const std = @import("std");
const yaml_loader = @import("yaml_loader.zig");

// Core version detection
pub const ApiVersion = @import("detector.zig").OpenApiVersion;
Expand Down Expand Up @@ -70,6 +71,7 @@ pub const UnifiedApiGenerator = @import("generators/unified/api_generator.zig").

// CLI argument types for code generation
pub const CliArgs = @import("cli.zig").CliArgs;
pub const yamlToJson = yaml_loader.yamlToJson;

/// Parse a JSON string containing an OpenAPI or Swagger specification and convert it to a unified document representation.
/// The caller is responsible for calling `deinit()` on the returned document.
Expand Down Expand Up @@ -125,6 +127,45 @@ pub fn parseToUnified(allocator: std.mem.Allocator, json_content: []const u8) !U
}
}

/// Detect the OpenAPI/Swagger version from a YAML specification.
pub fn detectVersionFromYaml(allocator: std.mem.Allocator, yaml_content: []const u8) !ApiVersion {
const json_content = try yamlToJson(allocator, yaml_content);
defer allocator.free(json_content);
return try detectVersion(allocator, json_content);
}

/// Parse a YAML string containing an OpenAPI v3.0 specification.
/// The caller is responsible for calling `deinit()` on the returned document.
pub fn parseOpenApiYaml(allocator: std.mem.Allocator, yaml_content: []const u8) !OpenApiDocument {
const json_content = try yamlToJson(allocator, yaml_content);
defer allocator.free(json_content);
return try OpenApiDocument.parseFromJson(allocator, json_content);
}

/// Parse a YAML string containing an OpenAPI v3.1 specification.
/// The caller is responsible for calling `deinit()` on the returned document.
pub fn parseOpenApi31Yaml(allocator: std.mem.Allocator, yaml_content: []const u8) !OpenApi31Document {
const json_content = try yamlToJson(allocator, yaml_content);
defer allocator.free(json_content);
return try OpenApi31Document.parseFromJson(allocator, json_content);
}

/// Parse a YAML string containing an OpenAPI v3.2 specification.
/// The caller is responsible for calling `deinit()` on the returned document.
pub fn parseOpenApi32Yaml(allocator: std.mem.Allocator, yaml_content: []const u8) !OpenApi32Document {
const json_content = try yamlToJson(allocator, yaml_content);
defer allocator.free(json_content);
return try OpenApi32Document.parseFromJson(allocator, json_content);
}

/// Parse a YAML string containing a Swagger v2.0 specification.
/// The caller is responsible for calling `deinit()` on the returned document.
pub fn parseSwaggerYaml(allocator: std.mem.Allocator, yaml_content: []const u8) !SwaggerDocument {
const json_content = try yamlToJson(allocator, yaml_content);
defer allocator.free(json_content);
return try SwaggerDocument.parseFromJson(allocator, json_content);
}

/// Parse a JSON string containing an OpenAPI v3.0 specification.
/// The caller is responsible for calling `deinit()` on the returned document.
///
Expand Down
2 changes: 2 additions & 0 deletions src/tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const swagger_v2_tests = @import("tests/swagger_v2_tests.zig");
const unified_converter_tests = @import("tests/unified_converter_tests.zig");
const comprehensive_converter_tests = @import("tests/comprehensive_converter_tests.zig");
const test_input_loader = @import("tests/test_input_loader.zig");
const yaml_loader_tests = @import("tests/yaml_loader_tests.zig");
const resource_wrapper_tests = @import("tests/resource_wrapper_tests.zig");
const model_typing_tests = @import("tests/model_typing_tests.zig");
comptime {
Expand All @@ -15,6 +16,7 @@ comptime {
_ = unified_converter_tests;
_ = comprehensive_converter_tests;
_ = test_input_loader;
_ = yaml_loader_tests;
_ = resource_wrapper_tests;
_ = model_typing_tests;
}
Loading
Loading