diff --git a/docs/json-value-typing-policy.md b/docs/json-value-typing-policy.md new file mode 100644 index 0000000..d95833d --- /dev/null +++ b/docs/json-value-typing-policy.md @@ -0,0 +1,104 @@ +# `std.json.Value` typing policy + +`openapi2zig` should generate typed Zig declarations when an OpenAPI schema gives enough structure, and use `std.json.Value` only when the schema is genuinely open-ended or ambiguous. + +## Current behavior + +The unified model generator maps schemas roughly as follows: + +- `$ref` => referenced Zig declaration name. +- `type: string` => `[]const u8`. +- `type: integer` => `i64`. +- `type: number` => `f64`. +- `type: boolean` => `bool`. +- `type: array` with `items` => `[]const T` when item type/ref is known, otherwise `[]const std.json.Value`. +- `type: object` without generated properties => `std.json.Value`. +- schema with `properties` => generated `struct`, even if `type` is omitted. +- unknown schema => `std.json.Value`. +- string enums currently stay string aliases/types, not closed Zig enums. +- OpenAPI 3.1 `allOf` object/ref-to-object schemas are merged during conversion when possible. +- OpenAPI 3.1 `oneOf` / `anyOf` with a discriminator and all-ref object variants emit `union(enum)` with generated discriminator parsing and a `raw: std.json.Value` fallback. +- Other `oneOf` / `anyOf` schemas still fall back to `std.json.Value`. + +The API generator also falls back to `std.json.Value` for ambiguous request/response schemas and for object/array cases where no named schema exists. + +## Desired mapping + +| OpenAPI schema shape | Generated Zig shape | +| --- | --- | +| Object with known `properties` | Generated `struct` | +| Object with `properties` but no `type` | Generated `struct` | +| Array with item schema | `[]const T` | +| `$ref` | Referenced Zig declaration | +| String enum | String alias or `[]const u8` until enum policy is chosen | +| Numeric/boolean enum | Underlying scalar or `std.json.Value` if mixed | +| `additionalProperties: true` / free-form object | `std.json.Value` now, future map type possible | +| `additionalProperties: {schema}` | Future `std.StringHashMap(T)` or equivalent owned map | +| `oneOf` / `anyOf` without discriminator | `std.json.Value` | +| `oneOf` / `anyOf` with discriminator and safe object refs | `union(enum)` with custom parse/stringify and `raw` fallback | +| `oneOf` / `anyOf` with discriminator but unsafe variants | `std.json.Value` with comment | +| `allOf` where all members are objects or refs to objects | Merged generated `struct` | +| `allOf` with conflicting fields | Prefer explicit fallback/comment, avoid silently wrong type | +| `allOf` mixing primitive and object | `std.json.Value` fallback | +| Nullable `T` | `?T` | +| Unknown/empty schema | `std.json.Value` | + +## `oneOf` / `anyOf` policy + +Discriminator unions become Zig `union(enum)` declarations when all variants are object refs and each target object has a single string enum value for the discriminator property. + +Generated unions include: + +```zig +raw: std.json.Value, +``` + +for unknown provider variants. The generated `jsonParse`/`jsonParseFromValue` parses known discriminator tags into typed variants and preserves unknown tags as raw JSON. + +Unsafe discriminator unions still emit a comment and fall back to `std.json.Value`: + +```zig +// OpenAPI oneOf with discriminator could not be generated safely; generator currently uses std.json.Value. +``` + +Without a discriminator, use `std.json.Value` to preserve pass-through behavior. + +## `extra_body` policy + +Extensible request structs can include: + +```zig +extra_body: ?std.json.Value = null, +``` + +The generated `jsonStringify` flattens `extra_body.object` into the request object. + +Callers must not include keys in `extra_body` that duplicate known generated fields. Current output writes known fields first, then extra fields, which can produce duplicate JSON object keys. This preserves pass-through but does not define override behavior. + +## Default headers ownership + +Generated clients store `default_headers` as borrowed slices: + +```zig +client.default_headers = &.{ + .{ .name = "HTTP-Referer", .value = "https://example.com" }, +}; +``` + +The caller must keep the header slice and all header name/value storage alive for the duration of each request that uses them. + +## Raw and result response policy + +Generated clients should expose: + +- parsed convenience endpoints: `op(...) !Owned(T)` +- endpoint-specific result endpoints: `opResult(...) !ApiResult(T)` +- endpoint-specific raw endpoints: `opRaw(...) !RawResponse` +- generic raw/result helpers for dynamic paths + +`ApiResult(T)` preserves raw status/body for: + +- non-2xx API responses via `.api_error` +- JSON parse failures via `.parse_error` + +The parsed convenience endpoint may still return an error, but callers that need body/status must use `opResult` or `opRaw`. diff --git a/docs/openai-generation-issues.md b/docs/openai-generation-issues.md new file mode 100644 index 0000000..1e989c8 --- /dev/null +++ b/docs/openai-generation-issues.md @@ -0,0 +1,397 @@ +# OpenAI SDK Generation Issues + +Validation date: 2026-04-25 + +This reflects current `openapi2zig` after commit `8195545 Fix real-world OpenAPI code generation`, tested from `openai.zig`. + +## Commands run + +```sh +cd ../openapi2zig +zig build test + +cd ../openai.zig +zig build generate +zig build test +``` + +Additional checks: + +- generated endpoint body compile smoke test: passed for all 241 generated public functions +- live OpenRouter basic chat call: reached API, failed during response parse with `error.UnknownField` + +## Current status + +Much improved: + +- Generated `src/api.zig` compiles. +- No empty structs remain. +- Major types now exist: + - `CreateResponse` + - `Response` + - `CreateChatCompletionRequest` +- Generated `Client` exists. +- Auth header exists. +- `base_url` override exists. +- Query params are no longer ignored. +- Endpoint functions return `Owned(T)` wrappers instead of dangling `parsed.value`. +- All generated endpoint function bodies compile when forced. + +Current generated file stats: + +- lines: 16,516 +- generated types: 929 +- generated functions: 242 +- empty structs: 0 +- dangling `parsed.value` returns: 0 +- ignored params: 0 +- `Owned(...)` returns: 234 +- `error.ResponseError` sites: 241 +- `std.json.Value` mentions: 828 + + +## Update: streaming/OpenRouter usability pass + +Implemented in the generator after the initial `8195545` pass: + +- Generated endpoint response parsing uses `.ignore_unknown_fields = true`. +- Generated runtime exposes `RawResponse`, `requestRaw`, `getRaw`, and `postJsonRaw` so callers can inspect status and body. +- Generated runtime includes a bounded dynamic SSE parser: + - LF and CRLF lines + - comments starting with `:` + - multiple `data:` lines joined with `\n` + - blank-line dispatch + - `data: [DONE]` stop + - 256 KiB max line size + - 1 MiB max event size + - uses `streamDelimiterLimit`, so line size is not tied to the HTTP transfer buffer +- OpenAI generation emits: + - `streamChatCompletion(client, requestBody, callback)` + - `streamResponse(client, requestBody, callback)` +- Stream helpers force `stream: true` in the JSON payload and send `Accept: text/event-stream`. +- Live OpenRouter streaming smoke passed with `openrouter/free`. +- `CreateChatCompletionRequest` and `CreateResponse` include flattened `extra_body`. +- Assistant chat messages can carry provider data because `ChatCompletionRequestMessage` remains `std.json.Value`; generated assistant message structs also include `reasoning_details` when present/needed. + +Still open: + +- Typed stream event parsing. Current stream callbacks receive raw `data:` bytes. +- Typed API error result union. Current typed endpoints still return `error.ResponseError`; raw helpers expose body/status. +- Multipart/form-data and binary response ergonomics. +- Resource module SDK shape. + +## Remaining issues + +### 1. `zig build generate` fails after writing file + +Observed: + +```text +Code generated successfully and written to 'src/api.zig'. +thread ... panic: incorrect alignment +.../std/hash_map.zig:784:44 in header +.../openapi2zig/src/models/common/document.zig:92:33 in Schema.deinit +.../openapi2zig/src/models/common/document.zig:231:39 in UnifiedDocument.deinit +.../openapi2zig/src/generator.zig:139:29 in generateCodeFromOpenApi31Document +``` + +`src/api.zig` is written and compiles, but process exits via panic, causing `zig build generate` failure. + +Likely cause: + +- ownership/deinit bug in unified schema conversion +- possibly copied `std.StringHashMap(Schema)` values aliasing map internals +- double deinit or deinit of moved/uninitialized map + +Acceptance: + +- `zig build generate` exits 0. +- No panic after writing output. +- Run under debug allocator with leak/double-free checks. + +### 2. Response parsing fails on unknown provider fields + +Live OpenRouter test: + +```zig +var client = openai.Client.init(allocator, init.io, api_key); +client.withBaseUrl("https://openrouter.ai/api/v1"); + +var response = try openai.createChatCompletion(&client, .{ + .model = "openrouter/free", + .messages = parsed_messages.value, +}); +``` + +Result: + +```text +error: UnknownField +std/json/static.zig:380:25 in innerParse +src/api.zig:6515 in createChatCompletion +``` + +Generated code currently parses with strict options: + +```zig +const parsed = try std.json.parseFromSlice(CreateChatCompletionResponse, allocator, body, .{}); +``` + +This should be: + +```zig +const parsed = try std.json.parseFromSlice(CreateChatCompletionResponse, allocator, body, .{ + .ignore_unknown_fields = true, +}); +``` + +Especially important for: + +- OpenRouter-compatible APIs +- OpenAI adding new fields before spec update +- event/message/provider extension fields + +Acceptance: + +- All generated response parsing uses `.ignore_unknown_fields = true` by default. +- Or client option controls strictness, default loose. + +### 3. No `reasoning_details` + +Observed: + +```sh +rg reasoning_details src/api.zig +# no results +``` + +OpenRouter reasoning carry-forward needs assistant message support: + +```zig +reasoning_details: ?std.json.Value = null, +``` + +Because OpenAI spec may not include this field, generator needs provider extension support. + +Acceptance: + +- Request structs can carry provider extension fields. +- Chat messages can preserve `reasoning_details` either via explicit field or `extra_body`/extra fields mechanism. + +### 4. No `extra_body` flattening + +Observed: + +```sh +rg extra_body src/api.zig +# no results +``` + +OpenRouter Python pattern: + +```python +extra_body={"reasoning": {"enabled": True}} +``` + +Generated request structs need an extension mechanism: + +```zig +extra_body: ?std.json.Value = null, +``` + +Serializer must flatten `extra_body` into root JSON object, not emit literal `extra_body`. + +Acceptance: + +- Generated request serialization supports provider-specific fields. +- `extra_body` flattened. +- Tests prove no `"extra_body"` key emitted. + +### 5. Streaming not implemented as streaming runtime + +Generated file has stream-related types and params, but no obvious SSE runtime/callback/iterator. + +Requirements: + +- raw SSE parser +- `[DONE]` support +- comments beginning `:` +- CRLF/LF +- multiple `data:` lines joined with `\n` +- max event size + +Acceptance: + +```zig +try openai.createResponseStream(&client, .{ ... }, callback); +``` + +or resource-equivalent generated API. + +### 6. Error bodies still discarded + +All endpoints still use: + +```zig +if (result.status.class() != .success) { + return error.ResponseError; +} +``` + +This loses response body with OpenAI error details. + +Acceptance: + +Short term: + +```zig +pub const RawResponse = struct { + status: std.http.Status, + body: []u8, +}; +``` + +Long term: + +```zig +pub const ApiResult(comptime T: type) = union(enum) { + ok: Owned(T), + api_error: Owned(ApiError), +}; +``` + +### 7. Multipart/binary endpoints still questionable + +Generated types include multipart body types: + +```zig +CreateVideoExtendMultipartBody +CreateVideoEditMultipartBody +CreateVideoMultipartBody +``` + +But generated functions appear to select JSON bodies for video endpoints: + +```zig +createVideo(... CreateVideoJsonBody) +CreateVideoExtend(... CreateVideoExtendJsonBody) +CreateVideoEdit(... CreateVideoEditJsonBody) +``` + +Also file/audio endpoints may still be JSON-only when spec requires multipart or binary response. + +Acceptance: + +- Inspect operation request content-type. +- Generate `multipart/form-data` runtime. +- Generate binary/raw response for file/audio content. +- Do not force `application/json` for multipart endpoints. + +### 8. `std.json.Value` overuse remains + +Current count: 828 mentions. + +Some are fine. But several important semantic fields still collapse to `std.json.Value`. + +Examples: + +- complex message content +- response input/output unions +- reasoning and stream event unions + +Acceptance: + +- Keep `std.json.Value` fallback for ambiguous schemas. +- Improve discriminated unions later. +- Prioritize chat/messages/responses event types. + +### 9. Top-level flat API remains + +Current generated API is flat: + +```zig +openai.createResponse(&client, ...) +openai.createChatCompletion(&client, ...) +``` + +This is usable, but eventual SDK mode should generate resources: + +```zig +client.responses.create(...) +client.chat.completions.create(...) +``` + +or: + +```zig +openai.resources.responses.create(&client, ...) +``` + +Flat API can remain for single-file/raw mode. + +## Reproduction snippets + +### Generate panic + +```sh +cd openai.zig +zig build generate +``` + +Expected: exit 0. +Current: writes file, then panic in `UnifiedDocument.deinit`. + +### Force all endpoint function bodies to compile + +```zig +const api = @import("src/api.zig"); + +test "force all generated endpoint function bodies" { + var run = false; + _ = &run; + if (run) { + _ = try api.createResponse(undefined, undefined); + // generated for all pub fns with undefined args + } +} +``` + +Current: passes for all 241 endpoint functions. + +### Basic OpenRouter live parse failure + +```zig +var client = openai.Client.init(allocator, init.io, api_key); +defer client.deinit(); +client.withBaseUrl("https://openrouter.ai/api/v1"); + +const messages_json = + \\[{"role":"user","content":"How many r's are in strawberry? Answer briefly."}] +; +const parsed_messages = try std.json.parseFromSlice([]const std.json.Value, allocator, messages_json, .{}); +defer parsed_messages.deinit(); + +var response = try openai.createChatCompletion(&client, .{ + .model = "openrouter/free", + .messages = parsed_messages.value, +}); +defer response.deinit(); +``` + +Current result: `error.UnknownField` while parsing `CreateChatCompletionResponse`. + +## Acceptance criteria for next pass + +Must: + +- `zig build generate` exits 0; no deinit panic. +- All generated response parsing ignores unknown fields by default. +- Basic OpenRouter chat call succeeds or at least returns inspectable non-2xx body. +- Error bodies are available via raw/detailed helper. + +Should: + +- Add `extra_body` flattening. +- Add `reasoning_details` extension support. +- Add first streaming/SSE runtime. +- Start multipart support or mark unsupported endpoints raw. diff --git a/generated/compile_generated.zig b/generated/compile_generated.zig index 8d00d2b..226bd5a 100644 --- a/generated/compile_generated.zig +++ b/generated/compile_generated.zig @@ -11,3 +11,144 @@ test "generated clients compile" { std.testing.refAllDecls(v31); std.testing.refAllDecls(v32); } + +const SseCallback = struct { + count: usize = 0, + + pub fn event(self: *SseCallback, data: []const u8) !void { + switch (self.count) { + 0 => try std.testing.expectEqualStrings("{\"x\":1}", data), + 1 => try std.testing.expectEqualStrings("a", data), + 2 => try std.testing.expectEqualStrings("a\nb", data), + else => return error.UnexpectedEvent, + } + self.count += 1; + } +}; + +test "generated SSE parser handles comments CRLF multiline and done" { + var callback: SseCallback = .{}; + try v3.parseSseBytes( + std.testing.allocator, + "data: {\"x\":1}\n\n" ++ + "data: [DONE]\n\n" ++ + "data: should-not-dispatch\n\n", + &callback, + ); + try std.testing.expectEqual(@as(usize, 1), callback.count); + + callback = .{}; + try v3.parseSseBytes( + std.testing.allocator, + ": keepalive\n\n" ++ + "data: {\"x\":1}\n\n" ++ + ": ignored\r\n" ++ + "data: a\r\n" ++ + "\r\n" ++ + "data: a\n" ++ + "data: b\n" ++ + "\n", + &callback, + ); + try std.testing.expectEqual(@as(usize, 3), callback.count); +} + +test "generated SSE parser bounds line and event size" { + const max_sse_line_size = 256 * 1024; + const max_sse_event_size = 1024 * 1024; + + var callback: SseCallback = .{}; + + const long_line = try std.testing.allocator.alloc(u8, max_sse_line_size + 1); + defer std.testing.allocator.free(long_line); + @memset(long_line, 'x'); + try std.testing.expectError(error.SseLineTooLong, v3.parseSseBytes(std.testing.allocator, long_line, &callback)); + + var input: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer input.deinit(); + const chunk = try std.testing.allocator.alloc(u8, 220 * 1024); + defer std.testing.allocator.free(chunk); + @memset(chunk, 'a'); + + var written: usize = 0; + while (written <= max_sse_event_size) : (written += chunk.len + 1) { + try input.writer.writeAll("data: "); + try input.writer.writeAll(chunk); + try input.writer.writeAll("\n"); + } + try input.writer.writeAll("\n"); + + try std.testing.expectError(error.SseEventTooLong, v3.parseSseBytes(std.testing.allocator, input.written(), &callback)); +} + +const TypedEvent = struct { + x: i64, +}; + +const TypedSseTestCallback = struct { + seen: i64 = 0, + + pub fn event(self: *TypedSseTestCallback, value: *TypedEvent) !void { + self.seen += value.x; + } +}; + +test "generated SSE parser can parse typed events" { + var callback: TypedSseTestCallback = .{}; + try v3.parseSseBytesTyped( + TypedEvent, + std.testing.allocator, + "data: {\"x\":1,\"ignored\":true}\n\n" ++ + "data: {\"x\":2}\n\n" ++ + "data: [DONE]\n\n", + &callback, + ); + try std.testing.expectEqual(@as(i64, 3), callback.seen); +} + +test "generated ApiResult parses success and keeps error body" { + const ok_body = try std.testing.allocator.dupe(u8, "{\"x\":42,\"ignored\":true}"); + var ok_result = try v3.parseRawResponse(TypedEvent, .{ + .allocator = std.testing.allocator, + .status = .ok, + .body = ok_body, + }); + defer ok_result.deinit(); + switch (ok_result) { + .ok => |*owned| try std.testing.expectEqual(@as(i64, 42), owned.value().x), + .api_error, .parse_error => return error.ExpectedOk, + } + + const error_body = try std.testing.allocator.dupe(u8, "{\"error\":\"bad\"}"); + var error_result = try v3.parseRawResponse(TypedEvent, .{ + .allocator = std.testing.allocator, + .status = .bad_request, + .body = error_body, + }); + defer error_result.deinit(); + switch (error_result) { + .ok, .parse_error => return error.ExpectedApiError, + .api_error => |raw| try std.testing.expectEqualStrings("{\"error\":\"bad\"}", raw.body), + } + + const invalid_body = try std.testing.allocator.dupe(u8, "{\"x\":\"not-an-int\"}"); + var parse_result = try v3.parseRawResponse(TypedEvent, .{ + .allocator = std.testing.allocator, + .status = .ok, + .body = invalid_body, + }); + defer parse_result.deinit(); + switch (parse_result) { + .ok, .api_error => return error.ExpectedParseError, + .parse_error => |parse_error| { + try std.testing.expectEqualStrings("{\"x\":\"not-an-int\"}", parse_error.raw.body); + try std.testing.expect(parse_error.error_name.len > 0); + }, + } +} + +test "generated endpoint parsing is loose" { + const source = @embedFile("generated_v3.zig"); + try std.testing.expect(std.mem.indexOf(u8, source, ", allocator, body, .{})") == null); + try std.testing.expect(std.mem.indexOf(u8, source, ".ignore_unknown_fields = true") != null); +} diff --git a/generated/generated_v2.zig b/generated/generated_v2.zig index 2a4801b..fb49820 100644 --- a/generated/generated_v2.zig +++ b/generated/generated_v2.zig @@ -11,7 +11,7 @@ pub const Category = struct { pub const Pet = struct { status: ?[]const u8 = null, - tags: ?[]const std.json.Value = null, + tags: ?[]const Tag = null, category: ?Category = null, id: ?i64 = null, name: []const u8, @@ -53,6 +53,361 @@ pub const ApiResponse = struct { // Generated Zig API client from OpenAPI /////////////////////////////////////////// +pub fn Owned(comptime T: type) type { + return struct { + allocator: std.mem.Allocator, + body: []u8, + parsed: std.json.Parsed(T), + + pub fn deinit(self: *@This()) void { + self.parsed.deinit(); + self.allocator.free(self.body); + } + + pub fn value(self: *@This()) *T { + return &self.parsed.value; + } + }; +} + +pub const RawResponse = struct { + allocator: std.mem.Allocator, + status: std.http.Status, + body: []u8, + + pub fn deinit(self: *@This()) void { + self.allocator.free(self.body); + } +}; + +pub const ParseErrorResponse = struct { + raw: RawResponse, + error_name: []const u8, +}; + +pub fn ApiResult(comptime T: type) type { + return union(enum) { + ok: Owned(T), + api_error: RawResponse, + parse_error: ParseErrorResponse, + + pub fn deinit(self: *@This()) void { + switch (self.*) { + .ok => |*value| value.deinit(), + .api_error => |*value| value.deinit(), + .parse_error => |*value| value.raw.deinit(), + } + } + }; +} + +pub const Client = struct { + allocator: std.mem.Allocator, + io: std.Io, + http: std.http.Client, + api_key: []const u8, + base_url: []const u8 = "https://petstore.swagger.io/v2", + organization: ?[]const u8 = null, + project: ?[]const u8 = null, + default_headers: []const std.http.Header = &.{}, + + pub fn init(allocator: std.mem.Allocator, io: std.Io, api_key: []const u8) Client { + return .{ + .allocator = allocator, + .io = io, + .http = .{ .allocator = allocator, .io = io }, + .api_key = api_key, + }; + } + + pub fn deinit(self: *Client) void { + self.http.deinit(); + } + + pub fn withBaseUrl(self: *Client, base_url: []const u8) void { + self.base_url = base_url; + } +}; + +fn isQueryChar(c: u8) bool { + return std.ascii.isAlphanumeric(c) or switch (c) { + '-', '.', '_', '~' => true, + else => false, + }; +} + +fn writeQueryComponent(writer: *std.Io.Writer, value: []const u8) !void { + try std.Uri.Component.percentEncode(writer, value, isQueryChar); +} + +fn writeQueryValue(writer: *std.Io.Writer, value: anytype) !void { + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .pointer => |ptr| { + if (ptr.size == .slice and ptr.child == u8) { + try writeQueryComponent(writer, value); + } else { + try std.json.Stringify.value(value, .{}, writer); + } + }, + .int, .comptime_int, .float, .comptime_float, .bool => try writer.print("{}", .{value}), + .@"enum" => try writeQueryComponent(writer, @tagName(value)), + else => try std.json.Stringify.value(value, .{}, writer), + } +} + +fn appendQueryParam(writer: *std.Io.Writer, first_query: *bool, name: []const u8, value: anytype) !void { + if (first_query.*) { + try writer.writeByte('?'); + first_query.* = false; + } else { + try writer.writeByte('&'); + } + try writeQueryComponent(writer, name); + try writer.writeByte('='); + try writeQueryValue(writer, value); +} + +pub fn requestRaw(client: *Client, method: std.http.Method, url: []const u8, payload: ?[]const u8) !RawResponse { + const allocator = client.allocator; + var headers = std.ArrayList(std.http.Header).empty; + defer headers.deinit(allocator); + const auth_header = try appendClientHeaders(allocator, &headers, client, payload != null, "application/json"); + defer if (auth_header) |value| allocator.free(value); + + const uri = try std.Uri.parse(url); + var response_body: std.Io.Writer.Allocating = .init(allocator); + defer response_body.deinit(); + + const result = try client.http.fetch(.{ + .location = .{ .uri = uri }, + .method = method, + .extra_headers = headers.items, + .payload = payload, + .response_writer = &response_body.writer, + }); + + return .{ + .allocator = allocator, + .status = result.status, + .body = try response_body.toOwnedSlice(), + }; +} + +pub fn getRaw(client: *Client, path: []const u8) !RawResponse { + const url = try std.fmt.allocPrint(client.allocator, "{s}{s}", .{ client.base_url, path }); + defer client.allocator.free(url); + return requestRaw(client, .GET, url, null); +} + +pub fn postJsonRaw(client: *Client, path: []const u8, payload: anytype) !RawResponse { + const allocator = client.allocator; + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(payload, .{ .emit_null_optional_fields = false }, &str.writer); + + const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ client.base_url, path }); + defer allocator.free(url); + return requestRaw(client, .POST, url, str.written()); +} + +pub fn parseRawResponse(comptime T: type, raw: RawResponse) !ApiResult(T) { + if (raw.status.class() != .success) return .{ .api_error = raw }; + const parsed = std.json.parseFromSlice(T, raw.allocator, raw.body, .{ .ignore_unknown_fields = true }) catch |err| { + return .{ .parse_error = .{ .raw = raw, .error_name = @errorName(err) } }; + }; + return .{ .ok = .{ .allocator = raw.allocator, .body = raw.body, .parsed = parsed } }; +} + +pub fn getJsonResult(comptime T: type, client: *Client, path: []const u8) !ApiResult(T) { + return parseRawResponse(T, try getRaw(client, path)); +} + +pub fn postJsonResult(comptime T: type, client: *Client, path: []const u8, payload: anytype) !ApiResult(T) { + return parseRawResponse(T, try postJsonRaw(client, path, payload)); +} + +const max_sse_line_size = 256 * 1024; +const max_sse_event_size = 1024 * 1024; + +pub fn parseSseBytes(allocator: std.mem.Allocator, bytes: []const u8, callback: anytype) !void { + var reader: std.Io.Reader = .fixed(bytes); + try parseSseReader(allocator, &reader, callback); +} + +pub fn parseSseReader(allocator: std.mem.Allocator, reader: *std.Io.Reader, callback: anytype) !void { + var line_buf: std.Io.Writer.Allocating = .init(allocator); + defer line_buf.deinit(); + + var event_data: std.Io.Writer.Allocating = .init(allocator); + defer event_data.deinit(); + + while (true) { + line_buf.clearRetainingCapacity(); + + _ = reader.streamDelimiterLimit(&line_buf.writer, '\n', .limited(max_sse_line_size)) catch |err| switch (err) { + error.StreamTooLong => return error.SseLineTooLong, + error.ReadFailed => return err, + error.WriteFailed => return err, + }; + + const ended_with_delimiter = blk: { + const byte = reader.peekByte() catch |err| switch (err) { + error.EndOfStream => break :blk false, + error.ReadFailed => return err, + }; + if (byte == '\n') { + _ = try reader.takeByte(); + break :blk true; + } + break :blk false; + }; + + if (try processSseLine(&event_data, line_buf.written(), callback)) return; + if (!ended_with_delimiter) break; + } + + _ = try dispatchSseEvent(&event_data, callback); +} + +fn processSseLine(event_data: *std.Io.Writer.Allocating, raw_line: []const u8, callback: anytype) !bool { + const line = std.mem.trimEnd(u8, raw_line, "\r"); + if (line.len == 0) return try dispatchSseEvent(event_data, callback); + if (line[0] == ':') return false; + + const colon = std.mem.indexOfScalar(u8, line, ':') orelse return false; + const field = line[0..colon]; + if (!std.mem.eql(u8, field, "data")) return false; + + var value = line[colon + 1 ..]; + if (value.len > 0 and value[0] == ' ') value = value[1..]; + const separator_len: usize = if (event_data.written().len == 0) 0 else 1; + if (event_data.written().len + separator_len + value.len > max_sse_event_size) return error.SseEventTooLong; + if (separator_len != 0) try event_data.writer.writeByte('\n'); + try event_data.writer.writeAll(value); + return false; +} + +fn dispatchSseEvent(event_data: *std.Io.Writer.Allocating, callback: anytype) !bool { + const data = event_data.written(); + if (data.len == 0) return false; + defer event_data.clearRetainingCapacity(); + + if (std.mem.eql(u8, data, "[DONE]")) return true; + try callback.event(data); + return false; +} + +fn TypedSseCallback(comptime T: type, comptime Callback: type) type { + return struct { + allocator: std.mem.Allocator, + callback: *Callback, + + pub fn event(self: *@This(), data: []const u8) !void { + var parsed = try std.json.parseFromSlice(T, self.allocator, data, .{ .ignore_unknown_fields = true }); + defer parsed.deinit(); + try self.callback.event(&parsed.value); + } + }; +} + +pub fn parseSseBytesTyped(comptime T: type, allocator: std.mem.Allocator, bytes: []const u8, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = allocator, .callback = callback }; + try parseSseBytes(allocator, bytes, &typed_callback); +} + +pub fn parseSseReaderTyped(comptime T: type, allocator: std.mem.Allocator, reader: *std.Io.Reader, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = allocator, .callback = callback }; + try parseSseReader(allocator, reader, &typed_callback); +} + +fn stringifyStreamRequest(allocator: std.mem.Allocator, requestBody: anytype) ![]u8 { + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, str.written(), .{ .ignore_unknown_fields = true }); + defer parsed.deinit(); + + if (parsed.value == .object) { + try parsed.value.object.put(parsed.arena.allocator(), "stream", .{ .bool = true }); + } + + var out: std.Io.Writer.Allocating = .init(allocator); + errdefer out.deinit(); + try std.json.Stringify.value(parsed.value, .{ .emit_null_optional_fields = false }, &out.writer); + return try out.toOwnedSlice(); +} + +fn streamJsonTyped(comptime T: type, client: *Client, path: []const u8, requestBody: anytype, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = client.allocator, .callback = callback }; + try streamJson(client, path, requestBody, &typed_callback); +} + +fn streamJson(client: *Client, path: []const u8, requestBody: anytype, callback: anytype) !void { + const allocator = client.allocator; + const payload = try stringifyStreamRequest(allocator, requestBody); + defer allocator.free(payload); + + var headers = std.ArrayList(std.http.Header).empty; + defer headers.deinit(allocator); + const auth_header = try appendClientHeaders(allocator, &headers, client, true, "text/event-stream"); + defer if (auth_header) |value| allocator.free(value); + + const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ client.base_url, path }); + defer allocator.free(url); + const uri = try std.Uri.parse(url); + + var req = try client.http.request(.POST, uri, .{ + .redirect_behavior = .unhandled, + .headers = .{ .accept_encoding = .{ .override = "identity" } }, + .extra_headers = headers.items, + }); + defer req.deinit(); + + req.transfer_encoding = .{ .content_length = payload.len }; + var body = try req.sendBodyUnflushed(&.{}); + try body.writer.writeAll(payload); + try body.end(); + try req.connection.?.flush(); + + var response = try req.receiveHead(&.{}); + if (response.head.status.class() != .success) return error.ResponseError; + + var transfer_buffer: [8 * 1024]u8 = undefined; + const reader = response.reader(&transfer_buffer); + parseSseReader(allocator, reader, callback) catch |err| switch (err) { + error.ReadFailed => return response.bodyErr() orelse err, + else => return err, + }; +} + +fn appendClientHeaders(allocator: std.mem.Allocator, headers: *std.ArrayList(std.http.Header), client: *Client, include_content_type: bool, accept: []const u8) !?[]u8 { + if (include_content_type) { + try headers.append(allocator, .{ .name = "Content-Type", .value = "application/json" }); + } + try headers.append(allocator, .{ .name = "Accept", .value = accept }); + + var auth_header: ?[]u8 = null; + if (client.api_key.len > 0) { + auth_header = try std.fmt.allocPrint(allocator, "Bearer {s}", .{client.api_key}); + try headers.append(allocator, .{ .name = "Authorization", .value = auth_header.? }); + } + if (client.organization) |organization| { + try headers.append(allocator, .{ .name = "OpenAI-Organization", .value = organization }); + } + if (client.project) |project| { + try headers.append(allocator, .{ .name = "OpenAI-Project", .value = project }); + } + for (client.default_headers) |header| { + try headers.append(allocator, header); + } + return auth_header; +} + ///////////////// // Summary: // Place an order for a pet @@ -60,33 +415,37 @@ pub const ApiResponse = struct { // Description: // // -pub fn placeOrder(allocator: std.mem.Allocator, io: std.Io, requestBody: Order) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn placeOrder(client: *Client, requestBody: Order) !Owned(Order) { + var result = try placeOrderResult(client, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn placeOrderRaw(client: *Client, requestBody: Order) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/store/order", .{client.base_url}); - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/store/order", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); var str: std.Io.Writer.Allocating = .init(allocator); defer str.deinit(); - try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); - const payload = str.written(); + const payload: ?[]const u8 = str.written(); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.POST, - .extra_headers = headers, - .payload = payload, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +pub fn placeOrderResult(client: *Client, requestBody: Order) !ApiResult(Order) { + return parseRawResponse(Order, try placeOrderRaw(client, requestBody)); } ///////////////// @@ -96,30 +455,35 @@ pub fn placeOrder(allocator: std.mem.Allocator, io: std.Io, requestBody: Order) // Description: // // -pub fn uploadFile(allocator: std.mem.Allocator, io: std.Io, petId: i64, additionalMetadata: []const u8, file: []const u8) !void { +pub fn uploadFile(client: *Client, petId: i64, additionalMetadata: []const u8, file: []const u8) !Owned(ApiResponse) { + var result = try uploadFileResult(client, petId, additionalMetadata, file); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} + +pub fn uploadFileRaw(client: *Client, petId: i64, additionalMetadata: []const u8, file: []const u8) !RawResponse { + const allocator = client.allocator; _ = additionalMetadata; _ = file; - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/{d}/uploadImage", .{ client.base_url, petId }); + const payload: ?[]const u8 = null; - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/pet/{d}/uploadImage", .{ - petId, - }); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.POST, - .extra_headers = headers, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } +pub fn uploadFileResult(client: *Client, petId: i64, additionalMetadata: []const u8, file: []const u8) !ApiResult(ApiResponse) { + return parseRawResponse(ApiResponse, try uploadFileRaw(client, petId, additionalMetadata, file)); } ///////////////// @@ -129,36 +493,33 @@ pub fn uploadFile(allocator: std.mem.Allocator, io: std.Io, petId: i64, addition // Description: // Returns a single pet // -pub fn getPetById(allocator: std.mem.Allocator, io: std.Io, petId: i64) !Pet { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); - - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; - - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/pet/{d}", .{petId}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - var response_body: std.Io.Writer.Allocating = .init(allocator); - defer response_body.deinit(); - - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, - .response_writer = &response_body.writer, - }); - if (result.status.class() != .success) { - return error.ResponseError; +pub fn getPetById(client: *Client, petId: i64) !Owned(Pet) { + var result = try getPetByIdResult(client, petId); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, } +} - const body = response_body.written(); - const parsed = try std.json.parseFromSlice(Pet, allocator, body, .{}); - defer parsed.deinit(); +pub fn getPetByIdRaw(client: *Client, petId: i64) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/{d}", .{ client.base_url, petId }); + const payload: ?[]const u8 = null; - return parsed.value; + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn getPetByIdResult(client: *Client, petId: i64) !ApiResult(Pet) { + return parseRawResponse(Pet, try getPetByIdRaw(client, petId)); } ///////////////// @@ -168,30 +529,22 @@ pub fn getPetById(allocator: std.mem.Allocator, io: std.Io, petId: i64) !Pet { // Description: // // -pub fn updatePetWithForm(allocator: std.mem.Allocator, io: std.Io, petId: i64, name: []const u8, status: []const u8) !void { +pub fn updatePetWithForm(client: *Client, petId: i64, name: []const u8, status: []const u8) !void { + var raw = try updatePetWithFormRaw(client, petId, name, status); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} + +pub fn updatePetWithFormRaw(client: *Client, petId: i64, name: []const u8, status: []const u8) !RawResponse { + const allocator = client.allocator; _ = name; _ = status; - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/{d}", .{ client.base_url, petId }); + const payload: ?[]const u8 = null; - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; - - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/pet/{d}", .{ - petId, - }); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.POST, - .extra_headers = headers, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); } ///////////////// @@ -201,29 +554,21 @@ pub fn updatePetWithForm(allocator: std.mem.Allocator, io: std.Io, petId: i64, n // Description: // // -pub fn deletePet(allocator: std.mem.Allocator, io: std.Io, api_key: []const u8, petId: i64) !void { - _ = api_key; - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn deletePet(client: *Client, api_key: []const u8, petId: i64) !void { + var raw = try deletePetRaw(client, api_key, petId); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn deletePetRaw(client: *Client, api_key: []const u8, petId: i64) !RawResponse { + const allocator = client.allocator; + _ = api_key; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/{d}", .{ client.base_url, petId }); + const payload: ?[]const u8 = null; - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/pet/{d}", .{ - petId, - }); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.DELETE, - .extra_headers = headers, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.DELETE, uri_buf.written(), payload); } ///////////////// @@ -233,37 +578,35 @@ pub fn deletePet(allocator: std.mem.Allocator, io: std.Io, api_key: []const u8, // Description: // Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. // -pub fn findPetsByTags(allocator: std.mem.Allocator, io: std.Io, tags: []const u8) ![]const u8 { - _ = tags; - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); - - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; - - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/pet/findByTags", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - var response_body: std.Io.Writer.Allocating = .init(allocator); - defer response_body.deinit(); - - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, - .response_writer = &response_body.writer, - }); - if (result.status.class() != .success) { - return error.ResponseError; +pub fn findPetsByTags(client: *Client, tags: []const std.json.Value) !Owned([]const std.json.Value) { + var result = try findPetsByTagsResult(client, tags); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, } +} - const body = response_body.written(); - const parsed = try std.json.parseFromSlice([]const u8, allocator, body, .{}); - defer parsed.deinit(); +pub fn findPetsByTagsRaw(client: *Client, tags: []const std.json.Value) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/findByTags", .{client.base_url}); + var first_query = true; + try appendQueryParam(&uri_buf.writer, &first_query, "tags", tags); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} - return parsed.value; +pub fn findPetsByTagsResult(client: *Client, tags: []const std.json.Value) !ApiResult([]const std.json.Value) { + return parseRawResponse([]const std.json.Value, try findPetsByTagsRaw(client, tags)); } ///////////////// @@ -273,38 +616,36 @@ pub fn findPetsByTags(allocator: std.mem.Allocator, io: std.Io, tags: []const u8 // Description: // // -pub fn loginUser(allocator: std.mem.Allocator, io: std.Io, username: []const u8, password: []const u8) ![]const u8 { - _ = username; - _ = password; - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); - - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; - - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/user/login", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - var response_body: std.Io.Writer.Allocating = .init(allocator); - defer response_body.deinit(); - - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, - .response_writer = &response_body.writer, - }); - if (result.status.class() != .success) { - return error.ResponseError; +pub fn loginUser(client: *Client, username: []const u8, password: []const u8) !Owned([]const u8) { + var result = try loginUserResult(client, username, password); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, } +} - const body = response_body.written(); - const parsed = try std.json.parseFromSlice([]const u8, allocator, body, .{}); - defer parsed.deinit(); +pub fn loginUserRaw(client: *Client, username: []const u8, password: []const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/login", .{client.base_url}); + var first_query = true; + try appendQueryParam(&uri_buf.writer, &first_query, "username", username); + try appendQueryParam(&uri_buf.writer, &first_query, "password", password); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} - return parsed.value; +pub fn loginUserResult(client: *Client, username: []const u8, password: []const u8) !ApiResult([]const u8) { + return parseRawResponse([]const u8, try loginUserRaw(client, username, password)); } ///////////////// @@ -314,33 +655,24 @@ pub fn loginUser(allocator: std.mem.Allocator, io: std.Io, username: []const u8, // Description: // // -pub fn createUsersWithArrayInput(allocator: std.mem.Allocator, io: std.Io, requestBody: []const u8) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn createUsersWithArrayInput(client: *Client, requestBody: []const std.json.Value) !void { + var raw = try createUsersWithArrayInputRaw(client, requestBody); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn createUsersWithArrayInputRaw(client: *Client, requestBody: []const std.json.Value) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/createWithArray", .{client.base_url}); - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/user/createWithArray", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); var str: std.Io.Writer.Allocating = .init(allocator); defer str.deinit(); - try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); - const payload = str.written(); + const payload: ?[]const u8 = str.written(); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.POST, - .extra_headers = headers, - .payload = payload, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); } ///////////////// @@ -350,37 +682,35 @@ pub fn createUsersWithArrayInput(allocator: std.mem.Allocator, io: std.Io, reque // Description: // Multiple status values can be provided with comma separated strings // -pub fn findPetsByStatus(allocator: std.mem.Allocator, io: std.Io, status: []const u8) ![]const u8 { - _ = status; - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); - - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; - - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/pet/findByStatus", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - var response_body: std.Io.Writer.Allocating = .init(allocator); - defer response_body.deinit(); - - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, - .response_writer = &response_body.writer, - }); - if (result.status.class() != .success) { - return error.ResponseError; +pub fn findPetsByStatus(client: *Client, status: []const std.json.Value) !Owned([]const std.json.Value) { + var result = try findPetsByStatusResult(client, status); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, } +} - const body = response_body.written(); - const parsed = try std.json.parseFromSlice([]const u8, allocator, body, .{}); - defer parsed.deinit(); +pub fn findPetsByStatusRaw(client: *Client, status: []const std.json.Value) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/findByStatus", .{client.base_url}); + var first_query = true; + try appendQueryParam(&uri_buf.writer, &first_query, "status", status); + const payload: ?[]const u8 = null; - return parsed.value; + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn findPetsByStatusResult(client: *Client, status: []const std.json.Value) !ApiResult([]const std.json.Value) { + return parseRawResponse([]const std.json.Value, try findPetsByStatusRaw(client, status)); } ///////////////// @@ -390,36 +720,33 @@ pub fn findPetsByStatus(allocator: std.mem.Allocator, io: std.Io, status: []cons // Description: // Returns a map of status codes to quantities // -pub fn getInventory(allocator: std.mem.Allocator, io: std.Io) !std.json.Value { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); - - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; - - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/store/inventory", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - var response_body: std.Io.Writer.Allocating = .init(allocator); - defer response_body.deinit(); - - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, - .response_writer = &response_body.writer, - }); - if (result.status.class() != .success) { - return error.ResponseError; +pub fn getInventory(client: *Client) !Owned(std.json.Value) { + var result = try getInventoryResult(client); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, } +} - const body = response_body.written(); - const parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); - defer parsed.deinit(); +pub fn getInventoryRaw(client: *Client) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/store/inventory", .{client.base_url}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} - return parsed.value; +pub fn getInventoryResult(client: *Client) !ApiResult(std.json.Value) { + return parseRawResponse(std.json.Value, try getInventoryRaw(client)); } ///////////////// @@ -429,36 +756,33 @@ pub fn getInventory(allocator: std.mem.Allocator, io: std.Io) !std.json.Value { // Description: // // -pub fn getUserByName(allocator: std.mem.Allocator, io: std.Io, username: []const u8) !User { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); - - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; - - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/user/{s}", .{username}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - var response_body: std.Io.Writer.Allocating = .init(allocator); - defer response_body.deinit(); - - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, - .response_writer = &response_body.writer, - }); - if (result.status.class() != .success) { - return error.ResponseError; +pub fn getUserByName(client: *Client, username: []const u8) !Owned(User) { + var result = try getUserByNameResult(client, username); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, } +} - const body = response_body.written(); - const parsed = try std.json.parseFromSlice(User, allocator, body, .{}); - defer parsed.deinit(); +pub fn getUserByNameRaw(client: *Client, username: []const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/{s}", .{ client.base_url, username }); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} - return parsed.value; +pub fn getUserByNameResult(client: *Client, username: []const u8) !ApiResult(User) { + return parseRawResponse(User, try getUserByNameRaw(client, username)); } ///////////////// @@ -468,35 +792,24 @@ pub fn getUserByName(allocator: std.mem.Allocator, io: std.Io, username: []const // Description: // This can only be done by the logged in user. // -pub fn updateUser(allocator: std.mem.Allocator, io: std.Io, username: []const u8, requestBody: User) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn updateUser(client: *Client, username: []const u8, requestBody: User) !void { + var raw = try updateUserRaw(client, username, requestBody); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn updateUserRaw(client: *Client, username: []const u8, requestBody: User) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/{s}", .{ client.base_url, username }); - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/user/{s}", .{ - username, - }); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); var str: std.Io.Writer.Allocating = .init(allocator); defer str.deinit(); - try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); - const payload = str.written(); + const payload: ?[]const u8 = str.written(); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.PUT, - .extra_headers = headers, - .payload = payload, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.PUT, uri_buf.written(), payload); } ///////////////// @@ -506,26 +819,20 @@ pub fn updateUser(allocator: std.mem.Allocator, io: std.Io, username: []const u8 // Description: // This can only be done by the logged in user. // -pub fn deleteUser(allocator: std.mem.Allocator, io: std.Io, username: []const u8) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn deleteUser(client: *Client, username: []const u8) !void { + var raw = try deleteUserRaw(client, username); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn deleteUserRaw(client: *Client, username: []const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/{s}", .{ client.base_url, username }); + const payload: ?[]const u8 = null; - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/user/{s}", .{username}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.DELETE, - .extra_headers = headers, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.DELETE, uri_buf.written(), payload); } ///////////////// @@ -535,33 +842,24 @@ pub fn deleteUser(allocator: std.mem.Allocator, io: std.Io, username: []const u8 // Description: // This can only be done by the logged in user. // -pub fn createUser(allocator: std.mem.Allocator, io: std.Io, requestBody: User) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn createUser(client: *Client, requestBody: User) !void { + var raw = try createUserRaw(client, requestBody); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn createUserRaw(client: *Client, requestBody: User) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user", .{client.base_url}); - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/user", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); var str: std.Io.Writer.Allocating = .init(allocator); defer str.deinit(); - try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); - const payload = str.written(); + const payload: ?[]const u8 = str.written(); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.POST, - .extra_headers = headers, - .payload = payload, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); } ///////////////// @@ -571,33 +869,24 @@ pub fn createUser(allocator: std.mem.Allocator, io: std.Io, requestBody: User) ! // Description: // // -pub fn createUsersWithListInput(allocator: std.mem.Allocator, io: std.Io, requestBody: []const u8) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn createUsersWithListInput(client: *Client, requestBody: []const std.json.Value) !void { + var raw = try createUsersWithListInputRaw(client, requestBody); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn createUsersWithListInputRaw(client: *Client, requestBody: []const std.json.Value) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/createWithList", .{client.base_url}); - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/user/createWithList", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); var str: std.Io.Writer.Allocating = .init(allocator); defer str.deinit(); - try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); - const payload = str.written(); + const payload: ?[]const u8 = str.written(); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.POST, - .extra_headers = headers, - .payload = payload, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); } ///////////////// @@ -607,33 +896,24 @@ pub fn createUsersWithListInput(allocator: std.mem.Allocator, io: std.Io, reques // Description: // // -pub fn addPet(allocator: std.mem.Allocator, io: std.Io, requestBody: Pet) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn addPet(client: *Client, requestBody: Pet) !void { + var raw = try addPetRaw(client, requestBody); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn addPetRaw(client: *Client, requestBody: Pet) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet", .{client.base_url}); - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/pet", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); var str: std.Io.Writer.Allocating = .init(allocator); defer str.deinit(); - try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); - const payload = str.written(); + const payload: ?[]const u8 = str.written(); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.POST, - .extra_headers = headers, - .payload = payload, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); } ///////////////// @@ -643,33 +923,24 @@ pub fn addPet(allocator: std.mem.Allocator, io: std.Io, requestBody: Pet) !void // Description: // // -pub fn updatePet(allocator: std.mem.Allocator, io: std.Io, requestBody: Pet) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn updatePet(client: *Client, requestBody: Pet) !void { + var raw = try updatePetRaw(client, requestBody); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn updatePetRaw(client: *Client, requestBody: Pet) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet", .{client.base_url}); - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/pet", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); var str: std.Io.Writer.Allocating = .init(allocator); defer str.deinit(); - try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); - const payload = str.written(); + const payload: ?[]const u8 = str.written(); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.PUT, - .extra_headers = headers, - .payload = payload, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.PUT, uri_buf.written(), payload); } ///////////////// @@ -679,36 +950,33 @@ pub fn updatePet(allocator: std.mem.Allocator, io: std.Io, requestBody: Pet) !vo // Description: // For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions // -pub fn getOrderById(allocator: std.mem.Allocator, io: std.Io, orderId: i64) !Order { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); - - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; - - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/store/order/{d}", .{orderId}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - var response_body: std.Io.Writer.Allocating = .init(allocator); - defer response_body.deinit(); - - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, - .response_writer = &response_body.writer, - }); - if (result.status.class() != .success) { - return error.ResponseError; +pub fn getOrderById(client: *Client, orderId: i64) !Owned(Order) { + var result = try getOrderByIdResult(client, orderId); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, } +} - const body = response_body.written(); - const parsed = try std.json.parseFromSlice(Order, allocator, body, .{}); - defer parsed.deinit(); +pub fn getOrderByIdRaw(client: *Client, orderId: i64) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/store/order/{d}", .{ client.base_url, orderId }); + const payload: ?[]const u8 = null; - return parsed.value; + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn getOrderByIdResult(client: *Client, orderId: i64) !ApiResult(Order) { + return parseRawResponse(Order, try getOrderByIdRaw(client, orderId)); } ///////////////// @@ -718,26 +986,20 @@ pub fn getOrderById(allocator: std.mem.Allocator, io: std.Io, orderId: i64) !Ord // Description: // For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors // -pub fn deleteOrder(allocator: std.mem.Allocator, io: std.Io, orderId: i64) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn deleteOrder(client: *Client, orderId: i64) !void { + var raw = try deleteOrderRaw(client, orderId); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn deleteOrderRaw(client: *Client, orderId: i64) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/store/order/{d}", .{ client.base_url, orderId }); + const payload: ?[]const u8 = null; - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/store/order/{d}", .{orderId}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.DELETE, - .extra_headers = headers, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.DELETE, uri_buf.written(), payload); } ///////////////// @@ -747,24 +1009,136 @@ pub fn deleteOrder(allocator: std.mem.Allocator, io: std.Io, orderId: i64) !void // Description: // // -pub fn logoutUser(allocator: std.mem.Allocator, io: std.Io) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn logoutUser(client: *Client) !void { + var raw = try logoutUserRaw(client); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn logoutUserRaw(client: *Client) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/logout", .{client.base_url}); + const payload: ?[]const u8 = null; - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore.swagger.io/v2/user/logout", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); } + +pub const resources = struct { + pub const pet = struct { + pub fn addpet(client: *Client, requestBody: Pet) !void { + return addPet(client, requestBody); + } + pub fn delete(client: *Client, api_key: []const u8, petId: i64) !void { + return deletePet(client, api_key, petId); + } + pub fn get(client: *Client, petId: i64) !Owned(Pet) { + return getPetById(client, petId); + } + pub fn getResult(client: *Client, petId: i64) !ApiResult(Pet) { + return getPetByIdResult(client, petId); + } + pub fn updatepet_(client: *Client, requestBody: Pet) !void { + return updatePet(client, requestBody); + } + pub fn updatepetwithform_(client: *Client, petId: i64, name: []const u8, status: []const u8) !void { + return updatePetWithForm(client, petId, name, status); + } + pub const findbystatus = struct { + pub fn findpetsbystatus(client: *Client, status: []const std.json.Value) !Owned([]const std.json.Value) { + return findPetsByStatus(client, status); + } + pub fn findpetsbystatusResult(client: *Client, status: []const std.json.Value) !ApiResult([]const std.json.Value) { + return findPetsByStatusResult(client, status); + } + }; + pub const findbytags = struct { + pub fn findpetsbytags(client: *Client, tags: []const std.json.Value) !Owned([]const std.json.Value) { + return findPetsByTags(client, tags); + } + pub fn findpetsbytagsResult(client: *Client, tags: []const std.json.Value) !ApiResult([]const std.json.Value) { + return findPetsByTagsResult(client, tags); + } + }; + pub const uploadimage = struct { + pub fn uploadfile(client: *Client, petId: i64, additionalMetadata: []const u8, file: []const u8) !Owned(ApiResponse) { + return uploadFile(client, petId, additionalMetadata, file); + } + pub fn uploadfileResult(client: *Client, petId: i64, additionalMetadata: []const u8, file: []const u8) !ApiResult(ApiResponse) { + return uploadFileResult(client, petId, additionalMetadata, file); + } + }; + }; + pub const store = struct { + pub const inventory = struct { + pub fn get(client: *Client) !Owned(std.json.Value) { + return getInventory(client); + } + pub fn getResult(client: *Client) !ApiResult(std.json.Value) { + return getInventoryResult(client); + } + }; + pub const order = struct { + pub fn delete(client: *Client, orderId: i64) !void { + return deleteOrder(client, orderId); + } + pub fn get(client: *Client, orderId: i64) !Owned(Order) { + return getOrderById(client, orderId); + } + pub fn getResult(client: *Client, orderId: i64) !ApiResult(Order) { + return getOrderByIdResult(client, orderId); + } + pub fn placeorder(client: *Client, requestBody: Order) !Owned(Order) { + return placeOrder(client, requestBody); + } + pub fn placeorderResult(client: *Client, requestBody: Order) !ApiResult(Order) { + return placeOrderResult(client, requestBody); + } + }; + }; + pub const user = struct { + pub fn create(client: *Client, requestBody: User) !void { + return createUser(client, requestBody); + } + pub fn delete(client: *Client, username: []const u8) !void { + return deleteUser(client, username); + } + pub fn get(client: *Client, username: []const u8) !Owned(User) { + return getUserByName(client, username); + } + pub fn getResult(client: *Client, username: []const u8) !ApiResult(User) { + return getUserByNameResult(client, username); + } + pub fn update(client: *Client, username: []const u8, requestBody: User) !void { + return updateUser(client, username, requestBody); + } + pub const createwitharray = struct { + pub fn create(client: *Client, requestBody: []const std.json.Value) !void { + return createUsersWithArrayInput(client, requestBody); + } + }; + pub const createwithlist = struct { + pub fn create(client: *Client, requestBody: []const std.json.Value) !void { + return createUsersWithListInput(client, requestBody); + } + }; + pub const login = struct { + pub fn loginuser(client: *Client, username: []const u8, password: []const u8) !Owned([]const u8) { + return loginUser(client, username, password); + } + pub fn loginuserResult(client: *Client, username: []const u8, password: []const u8) !ApiResult([]const u8) { + return loginUserResult(client, username, password); + } + }; + pub const logout = struct { + pub fn logoutuser(client: *Client) !void { + return logoutUser(client); + } + }; + }; +}; + +pub const pet = resources.pet; +pub const store = resources.store; +pub const user = resources.user; diff --git a/generated/generated_v3.zig b/generated/generated_v3.zig index 99b216e..f07807b 100644 --- a/generated/generated_v3.zig +++ b/generated/generated_v3.zig @@ -31,14 +31,14 @@ pub const Order = struct { }; pub const Customer = struct { - address: ?[]const std.json.Value = null, + address: ?[]const Address = null, id: ?i64 = null, username: ?[]const u8 = null, }; pub const Pet = struct { status: ?[]const u8 = null, - tags: ?[]const std.json.Value = null, + tags: ?[]const Tag = null, category: ?Category = null, id: ?i64 = null, name: []const u8, @@ -66,6 +66,361 @@ pub const ApiResponse = struct { // Generated Zig API client from OpenAPI /////////////////////////////////////////// +pub fn Owned(comptime T: type) type { + return struct { + allocator: std.mem.Allocator, + body: []u8, + parsed: std.json.Parsed(T), + + pub fn deinit(self: *@This()) void { + self.parsed.deinit(); + self.allocator.free(self.body); + } + + pub fn value(self: *@This()) *T { + return &self.parsed.value; + } + }; +} + +pub const RawResponse = struct { + allocator: std.mem.Allocator, + status: std.http.Status, + body: []u8, + + pub fn deinit(self: *@This()) void { + self.allocator.free(self.body); + } +}; + +pub const ParseErrorResponse = struct { + raw: RawResponse, + error_name: []const u8, +}; + +pub fn ApiResult(comptime T: type) type { + return union(enum) { + ok: Owned(T), + api_error: RawResponse, + parse_error: ParseErrorResponse, + + pub fn deinit(self: *@This()) void { + switch (self.*) { + .ok => |*value| value.deinit(), + .api_error => |*value| value.deinit(), + .parse_error => |*value| value.raw.deinit(), + } + } + }; +} + +pub const Client = struct { + allocator: std.mem.Allocator, + io: std.Io, + http: std.http.Client, + api_key: []const u8, + base_url: []const u8 = "https://petstore3.swagger.io/api/v3", + organization: ?[]const u8 = null, + project: ?[]const u8 = null, + default_headers: []const std.http.Header = &.{}, + + pub fn init(allocator: std.mem.Allocator, io: std.Io, api_key: []const u8) Client { + return .{ + .allocator = allocator, + .io = io, + .http = .{ .allocator = allocator, .io = io }, + .api_key = api_key, + }; + } + + pub fn deinit(self: *Client) void { + self.http.deinit(); + } + + pub fn withBaseUrl(self: *Client, base_url: []const u8) void { + self.base_url = base_url; + } +}; + +fn isQueryChar(c: u8) bool { + return std.ascii.isAlphanumeric(c) or switch (c) { + '-', '.', '_', '~' => true, + else => false, + }; +} + +fn writeQueryComponent(writer: *std.Io.Writer, value: []const u8) !void { + try std.Uri.Component.percentEncode(writer, value, isQueryChar); +} + +fn writeQueryValue(writer: *std.Io.Writer, value: anytype) !void { + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .pointer => |ptr| { + if (ptr.size == .slice and ptr.child == u8) { + try writeQueryComponent(writer, value); + } else { + try std.json.Stringify.value(value, .{}, writer); + } + }, + .int, .comptime_int, .float, .comptime_float, .bool => try writer.print("{}", .{value}), + .@"enum" => try writeQueryComponent(writer, @tagName(value)), + else => try std.json.Stringify.value(value, .{}, writer), + } +} + +fn appendQueryParam(writer: *std.Io.Writer, first_query: *bool, name: []const u8, value: anytype) !void { + if (first_query.*) { + try writer.writeByte('?'); + first_query.* = false; + } else { + try writer.writeByte('&'); + } + try writeQueryComponent(writer, name); + try writer.writeByte('='); + try writeQueryValue(writer, value); +} + +pub fn requestRaw(client: *Client, method: std.http.Method, url: []const u8, payload: ?[]const u8) !RawResponse { + const allocator = client.allocator; + var headers = std.ArrayList(std.http.Header).empty; + defer headers.deinit(allocator); + const auth_header = try appendClientHeaders(allocator, &headers, client, payload != null, "application/json"); + defer if (auth_header) |value| allocator.free(value); + + const uri = try std.Uri.parse(url); + var response_body: std.Io.Writer.Allocating = .init(allocator); + defer response_body.deinit(); + + const result = try client.http.fetch(.{ + .location = .{ .uri = uri }, + .method = method, + .extra_headers = headers.items, + .payload = payload, + .response_writer = &response_body.writer, + }); + + return .{ + .allocator = allocator, + .status = result.status, + .body = try response_body.toOwnedSlice(), + }; +} + +pub fn getRaw(client: *Client, path: []const u8) !RawResponse { + const url = try std.fmt.allocPrint(client.allocator, "{s}{s}", .{ client.base_url, path }); + defer client.allocator.free(url); + return requestRaw(client, .GET, url, null); +} + +pub fn postJsonRaw(client: *Client, path: []const u8, payload: anytype) !RawResponse { + const allocator = client.allocator; + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(payload, .{ .emit_null_optional_fields = false }, &str.writer); + + const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ client.base_url, path }); + defer allocator.free(url); + return requestRaw(client, .POST, url, str.written()); +} + +pub fn parseRawResponse(comptime T: type, raw: RawResponse) !ApiResult(T) { + if (raw.status.class() != .success) return .{ .api_error = raw }; + const parsed = std.json.parseFromSlice(T, raw.allocator, raw.body, .{ .ignore_unknown_fields = true }) catch |err| { + return .{ .parse_error = .{ .raw = raw, .error_name = @errorName(err) } }; + }; + return .{ .ok = .{ .allocator = raw.allocator, .body = raw.body, .parsed = parsed } }; +} + +pub fn getJsonResult(comptime T: type, client: *Client, path: []const u8) !ApiResult(T) { + return parseRawResponse(T, try getRaw(client, path)); +} + +pub fn postJsonResult(comptime T: type, client: *Client, path: []const u8, payload: anytype) !ApiResult(T) { + return parseRawResponse(T, try postJsonRaw(client, path, payload)); +} + +const max_sse_line_size = 256 * 1024; +const max_sse_event_size = 1024 * 1024; + +pub fn parseSseBytes(allocator: std.mem.Allocator, bytes: []const u8, callback: anytype) !void { + var reader: std.Io.Reader = .fixed(bytes); + try parseSseReader(allocator, &reader, callback); +} + +pub fn parseSseReader(allocator: std.mem.Allocator, reader: *std.Io.Reader, callback: anytype) !void { + var line_buf: std.Io.Writer.Allocating = .init(allocator); + defer line_buf.deinit(); + + var event_data: std.Io.Writer.Allocating = .init(allocator); + defer event_data.deinit(); + + while (true) { + line_buf.clearRetainingCapacity(); + + _ = reader.streamDelimiterLimit(&line_buf.writer, '\n', .limited(max_sse_line_size)) catch |err| switch (err) { + error.StreamTooLong => return error.SseLineTooLong, + error.ReadFailed => return err, + error.WriteFailed => return err, + }; + + const ended_with_delimiter = blk: { + const byte = reader.peekByte() catch |err| switch (err) { + error.EndOfStream => break :blk false, + error.ReadFailed => return err, + }; + if (byte == '\n') { + _ = try reader.takeByte(); + break :blk true; + } + break :blk false; + }; + + if (try processSseLine(&event_data, line_buf.written(), callback)) return; + if (!ended_with_delimiter) break; + } + + _ = try dispatchSseEvent(&event_data, callback); +} + +fn processSseLine(event_data: *std.Io.Writer.Allocating, raw_line: []const u8, callback: anytype) !bool { + const line = std.mem.trimEnd(u8, raw_line, "\r"); + if (line.len == 0) return try dispatchSseEvent(event_data, callback); + if (line[0] == ':') return false; + + const colon = std.mem.indexOfScalar(u8, line, ':') orelse return false; + const field = line[0..colon]; + if (!std.mem.eql(u8, field, "data")) return false; + + var value = line[colon + 1 ..]; + if (value.len > 0 and value[0] == ' ') value = value[1..]; + const separator_len: usize = if (event_data.written().len == 0) 0 else 1; + if (event_data.written().len + separator_len + value.len > max_sse_event_size) return error.SseEventTooLong; + if (separator_len != 0) try event_data.writer.writeByte('\n'); + try event_data.writer.writeAll(value); + return false; +} + +fn dispatchSseEvent(event_data: *std.Io.Writer.Allocating, callback: anytype) !bool { + const data = event_data.written(); + if (data.len == 0) return false; + defer event_data.clearRetainingCapacity(); + + if (std.mem.eql(u8, data, "[DONE]")) return true; + try callback.event(data); + return false; +} + +fn TypedSseCallback(comptime T: type, comptime Callback: type) type { + return struct { + allocator: std.mem.Allocator, + callback: *Callback, + + pub fn event(self: *@This(), data: []const u8) !void { + var parsed = try std.json.parseFromSlice(T, self.allocator, data, .{ .ignore_unknown_fields = true }); + defer parsed.deinit(); + try self.callback.event(&parsed.value); + } + }; +} + +pub fn parseSseBytesTyped(comptime T: type, allocator: std.mem.Allocator, bytes: []const u8, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = allocator, .callback = callback }; + try parseSseBytes(allocator, bytes, &typed_callback); +} + +pub fn parseSseReaderTyped(comptime T: type, allocator: std.mem.Allocator, reader: *std.Io.Reader, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = allocator, .callback = callback }; + try parseSseReader(allocator, reader, &typed_callback); +} + +fn stringifyStreamRequest(allocator: std.mem.Allocator, requestBody: anytype) ![]u8 { + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, str.written(), .{ .ignore_unknown_fields = true }); + defer parsed.deinit(); + + if (parsed.value == .object) { + try parsed.value.object.put(parsed.arena.allocator(), "stream", .{ .bool = true }); + } + + var out: std.Io.Writer.Allocating = .init(allocator); + errdefer out.deinit(); + try std.json.Stringify.value(parsed.value, .{ .emit_null_optional_fields = false }, &out.writer); + return try out.toOwnedSlice(); +} + +fn streamJsonTyped(comptime T: type, client: *Client, path: []const u8, requestBody: anytype, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = client.allocator, .callback = callback }; + try streamJson(client, path, requestBody, &typed_callback); +} + +fn streamJson(client: *Client, path: []const u8, requestBody: anytype, callback: anytype) !void { + const allocator = client.allocator; + const payload = try stringifyStreamRequest(allocator, requestBody); + defer allocator.free(payload); + + var headers = std.ArrayList(std.http.Header).empty; + defer headers.deinit(allocator); + const auth_header = try appendClientHeaders(allocator, &headers, client, true, "text/event-stream"); + defer if (auth_header) |value| allocator.free(value); + + const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ client.base_url, path }); + defer allocator.free(url); + const uri = try std.Uri.parse(url); + + var req = try client.http.request(.POST, uri, .{ + .redirect_behavior = .unhandled, + .headers = .{ .accept_encoding = .{ .override = "identity" } }, + .extra_headers = headers.items, + }); + defer req.deinit(); + + req.transfer_encoding = .{ .content_length = payload.len }; + var body = try req.sendBodyUnflushed(&.{}); + try body.writer.writeAll(payload); + try body.end(); + try req.connection.?.flush(); + + var response = try req.receiveHead(&.{}); + if (response.head.status.class() != .success) return error.ResponseError; + + var transfer_buffer: [8 * 1024]u8 = undefined; + const reader = response.reader(&transfer_buffer); + parseSseReader(allocator, reader, callback) catch |err| switch (err) { + error.ReadFailed => return response.bodyErr() orelse err, + else => return err, + }; +} + +fn appendClientHeaders(allocator: std.mem.Allocator, headers: *std.ArrayList(std.http.Header), client: *Client, include_content_type: bool, accept: []const u8) !?[]u8 { + if (include_content_type) { + try headers.append(allocator, .{ .name = "Content-Type", .value = "application/json" }); + } + try headers.append(allocator, .{ .name = "Accept", .value = accept }); + + var auth_header: ?[]u8 = null; + if (client.api_key.len > 0) { + auth_header = try std.fmt.allocPrint(allocator, "Bearer {s}", .{client.api_key}); + try headers.append(allocator, .{ .name = "Authorization", .value = auth_header.? }); + } + if (client.organization) |organization| { + try headers.append(allocator, .{ .name = "OpenAI-Organization", .value = organization }); + } + if (client.project) |project| { + try headers.append(allocator, .{ .name = "OpenAI-Project", .value = project }); + } + for (client.default_headers) |header| { + try headers.append(allocator, header); + } + return auth_header; +} + ///////////////// // Summary: // Place an order for a pet @@ -73,33 +428,37 @@ pub const ApiResponse = struct { // Description: // Place a new order in the store // -pub fn placeOrder(allocator: std.mem.Allocator, io: std.Io, requestBody: Order) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn placeOrder(client: *Client, requestBody: Order) !Owned(Order) { + var result = try placeOrderResult(client, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn placeOrderRaw(client: *Client, requestBody: Order) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/store/order", .{client.base_url}); - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/store/order", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); var str: std.Io.Writer.Allocating = .init(allocator); defer str.deinit(); - try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); - const payload = str.written(); + const payload: ?[]const u8 = str.written(); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.POST, - .extra_headers = headers, - .payload = payload, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +pub fn placeOrderResult(client: *Client, requestBody: Order) !ApiResult(Order) { + return parseRawResponse(Order, try placeOrderRaw(client, requestBody)); } ///////////////// @@ -109,36 +468,41 @@ pub fn placeOrder(allocator: std.mem.Allocator, io: std.Io, requestBody: Order) // Description: // // -pub fn uploadFile(allocator: std.mem.Allocator, io: std.Io, petId: []const u8, additionalMetadata: []const u8, requestBody: []const u8) !void { - _ = additionalMetadata; - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn uploadFile(client: *Client, petId: i64, additionalMetadata: ?[]const u8, requestBody: []const u8) !Owned(ApiResponse) { + var result = try uploadFileResult(client, petId, additionalMetadata, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn uploadFileRaw(client: *Client, petId: i64, additionalMetadata: ?[]const u8, requestBody: []const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/{d}/uploadImage", .{ client.base_url, petId }); + var first_query = true; + if (additionalMetadata) |value| { + try appendQueryParam(&uri_buf.writer, &first_query, "additionalMetadata", value); + } - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/pet/{s}/uploadImage", .{ - petId, - }); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); var str: std.Io.Writer.Allocating = .init(allocator); defer str.deinit(); - try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); - const payload = str.written(); + const payload: ?[]const u8 = str.written(); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.POST, - .extra_headers = headers, - .payload = payload, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +pub fn uploadFileResult(client: *Client, petId: i64, additionalMetadata: ?[]const u8, requestBody: []const u8) !ApiResult(ApiResponse) { + return parseRawResponse(ApiResponse, try uploadFileRaw(client, petId, additionalMetadata, requestBody)); } ///////////////// @@ -148,36 +512,33 @@ pub fn uploadFile(allocator: std.mem.Allocator, io: std.Io, petId: []const u8, a // Description: // Returns a single pet // -pub fn getPetById(allocator: std.mem.Allocator, io: std.Io, petId: []const u8) !Pet { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); - - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; - - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/pet/{s}", .{petId}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - var response_body: std.Io.Writer.Allocating = .init(allocator); - defer response_body.deinit(); - - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, - .response_writer = &response_body.writer, - }); - if (result.status.class() != .success) { - return error.ResponseError; +pub fn getPetById(client: *Client, petId: i64) !Owned(Pet) { + var result = try getPetByIdResult(client, petId); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, } +} - const body = response_body.written(); - const parsed = try std.json.parseFromSlice(Pet, allocator, body, .{}); - defer parsed.deinit(); +pub fn getPetByIdRaw(client: *Client, petId: i64) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/{d}", .{ client.base_url, petId }); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} - return parsed.value; +pub fn getPetByIdResult(client: *Client, petId: i64) !ApiResult(Pet) { + return parseRawResponse(Pet, try getPetByIdRaw(client, petId)); } ///////////////// @@ -187,30 +548,27 @@ pub fn getPetById(allocator: std.mem.Allocator, io: std.Io, petId: []const u8) ! // Description: // // -pub fn updatePetWithForm(allocator: std.mem.Allocator, io: std.Io, petId: []const u8, name: []const u8, status: []const u8) !void { - _ = name; - _ = status; - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); - - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn updatePetWithForm(client: *Client, petId: i64, name: ?[]const u8, status: ?[]const u8) !void { + var raw = try updatePetWithFormRaw(client, petId, name, status); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/pet/{s}", .{ - petId, - }); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.POST, - .extra_headers = headers, - }); - if (result.status.class() != .success) { - return error.ResponseError; +pub fn updatePetWithFormRaw(client: *Client, petId: i64, name: ?[]const u8, status: ?[]const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/{d}", .{ client.base_url, petId }); + var first_query = true; + if (name) |value| { + try appendQueryParam(&uri_buf.writer, &first_query, "name", value); + } + if (status) |value| { + try appendQueryParam(&uri_buf.writer, &first_query, "status", value); } + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); } ///////////////// @@ -220,29 +578,21 @@ pub fn updatePetWithForm(allocator: std.mem.Allocator, io: std.Io, petId: []cons // Description: // // -pub fn deletePet(allocator: std.mem.Allocator, io: std.Io, api_key: []const u8, petId: []const u8) !void { - _ = api_key; - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn deletePet(client: *Client, api_key: []const u8, petId: i64) !void { + var raw = try deletePetRaw(client, api_key, petId); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn deletePetRaw(client: *Client, api_key: []const u8, petId: i64) !RawResponse { + const allocator = client.allocator; + _ = api_key; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/{d}", .{ client.base_url, petId }); + const payload: ?[]const u8 = null; - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/pet/{s}", .{ - petId, - }); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.DELETE, - .extra_headers = headers, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.DELETE, uri_buf.written(), payload); } ///////////////// @@ -252,37 +602,37 @@ pub fn deletePet(allocator: std.mem.Allocator, io: std.Io, api_key: []const u8, // Description: // Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. // -pub fn findPetsByTags(allocator: std.mem.Allocator, io: std.Io, tags: []const u8) ![]const u8 { - _ = tags; - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); - - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; - - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/pet/findByTags", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - var response_body: std.Io.Writer.Allocating = .init(allocator); - defer response_body.deinit(); +pub fn findPetsByTags(client: *Client, tags: ?[]const u8) !Owned([]const std.json.Value) { + var result = try findPetsByTagsResult(client, tags); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, - .response_writer = &response_body.writer, - }); - if (result.status.class() != .success) { - return error.ResponseError; +pub fn findPetsByTagsRaw(client: *Client, tags: ?[]const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/findByTags", .{client.base_url}); + var first_query = true; + if (tags) |value| { + try appendQueryParam(&uri_buf.writer, &first_query, "tags", value); } + const payload: ?[]const u8 = null; - const body = response_body.written(); - const parsed = try std.json.parseFromSlice([]const u8, allocator, body, .{}); - defer parsed.deinit(); + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} - return parsed.value; +pub fn findPetsByTagsResult(client: *Client, tags: ?[]const u8) !ApiResult([]const std.json.Value) { + return parseRawResponse([]const std.json.Value, try findPetsByTagsRaw(client, tags)); } ///////////////// @@ -292,38 +642,40 @@ pub fn findPetsByTags(allocator: std.mem.Allocator, io: std.Io, tags: []const u8 // Description: // // -pub fn loginUser(allocator: std.mem.Allocator, io: std.Io, username: []const u8, password: []const u8) ![]const u8 { - _ = username; - _ = password; - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); - - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; - - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/user/login", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - var response_body: std.Io.Writer.Allocating = .init(allocator); - defer response_body.deinit(); +pub fn loginUser(client: *Client, username: ?[]const u8, password: ?[]const u8) !Owned([]const u8) { + var result = try loginUserResult(client, username, password); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, - .response_writer = &response_body.writer, - }); - if (result.status.class() != .success) { - return error.ResponseError; +pub fn loginUserRaw(client: *Client, username: ?[]const u8, password: ?[]const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/login", .{client.base_url}); + var first_query = true; + if (username) |value| { + try appendQueryParam(&uri_buf.writer, &first_query, "username", value); } + if (password) |value| { + try appendQueryParam(&uri_buf.writer, &first_query, "password", value); + } + const payload: ?[]const u8 = null; - const body = response_body.written(); - const parsed = try std.json.parseFromSlice([]const u8, allocator, body, .{}); - defer parsed.deinit(); + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} - return parsed.value; +pub fn loginUserResult(client: *Client, username: ?[]const u8, password: ?[]const u8) !ApiResult([]const u8) { + return parseRawResponse([]const u8, try loginUserRaw(client, username, password)); } ///////////////// @@ -333,37 +685,37 @@ pub fn loginUser(allocator: std.mem.Allocator, io: std.Io, username: []const u8, // Description: // Multiple status values can be provided with comma separated strings // -pub fn findPetsByStatus(allocator: std.mem.Allocator, io: std.Io, status: []const u8) ![]const u8 { - _ = status; - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); - - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; - - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/pet/findByStatus", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - var response_body: std.Io.Writer.Allocating = .init(allocator); - defer response_body.deinit(); +pub fn findPetsByStatus(client: *Client, status: ?[]const u8) !Owned([]const std.json.Value) { + var result = try findPetsByStatusResult(client, status); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, - .response_writer = &response_body.writer, - }); - if (result.status.class() != .success) { - return error.ResponseError; +pub fn findPetsByStatusRaw(client: *Client, status: ?[]const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/findByStatus", .{client.base_url}); + var first_query = true; + if (status) |value| { + try appendQueryParam(&uri_buf.writer, &first_query, "status", value); } + const payload: ?[]const u8 = null; - const body = response_body.written(); - const parsed = try std.json.parseFromSlice([]const u8, allocator, body, .{}); - defer parsed.deinit(); + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} - return parsed.value; +pub fn findPetsByStatusResult(client: *Client, status: ?[]const u8) !ApiResult([]const std.json.Value) { + return parseRawResponse([]const std.json.Value, try findPetsByStatusRaw(client, status)); } ///////////////// @@ -373,34 +725,33 @@ pub fn findPetsByStatus(allocator: std.mem.Allocator, io: std.Io, status: []cons // Description: // Returns a map of status codes to quantities // -pub fn getInventory(allocator: std.mem.Allocator, io: std.Io) !std.json.Value { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); - - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; - - const uri = try std.Uri.parse("https://petstore3.swagger.io/api/v3/store/inventory"); - var response_body: std.Io.Writer.Allocating = .init(allocator); - defer response_body.deinit(); - - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, - .response_writer = &response_body.writer, - }); - if (result.status.class() != .success) { - return error.ResponseError; +pub fn getInventory(client: *Client) !Owned(std.json.Value) { + var result = try getInventoryResult(client); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, } +} - const body = response_body.written(); - const parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); - defer parsed.deinit(); +pub fn getInventoryRaw(client: *Client) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/store/inventory", .{client.base_url}); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} - return parsed.value; +pub fn getInventoryResult(client: *Client) !ApiResult(std.json.Value) { + return parseRawResponse(std.json.Value, try getInventoryRaw(client)); } ///////////////// @@ -410,36 +761,33 @@ pub fn getInventory(allocator: std.mem.Allocator, io: std.Io) !std.json.Value { // Description: // // -pub fn getUserByName(allocator: std.mem.Allocator, io: std.Io, username: []const u8) !User { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); - - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; - - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/user/{s}", .{username}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - var response_body: std.Io.Writer.Allocating = .init(allocator); - defer response_body.deinit(); - - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, - .response_writer = &response_body.writer, - }); - if (result.status.class() != .success) { - return error.ResponseError; +pub fn getUserByName(client: *Client, username: []const u8) !Owned(User) { + var result = try getUserByNameResult(client, username); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, } +} - const body = response_body.written(); - const parsed = try std.json.parseFromSlice(User, allocator, body, .{}); - defer parsed.deinit(); +pub fn getUserByNameRaw(client: *Client, username: []const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/{s}", .{ client.base_url, username }); + const payload: ?[]const u8 = null; - return parsed.value; + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn getUserByNameResult(client: *Client, username: []const u8) !ApiResult(User) { + return parseRawResponse(User, try getUserByNameRaw(client, username)); } ///////////////// @@ -449,35 +797,24 @@ pub fn getUserByName(allocator: std.mem.Allocator, io: std.Io, username: []const // Description: // This can only be done by the logged in user. // -pub fn updateUser(allocator: std.mem.Allocator, io: std.Io, username: []const u8, requestBody: User) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn updateUser(client: *Client, username: []const u8, requestBody: User) !void { + var raw = try updateUserRaw(client, username, requestBody); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn updateUserRaw(client: *Client, username: []const u8, requestBody: User) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/{s}", .{ client.base_url, username }); - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/user/{s}", .{ - username, - }); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); var str: std.Io.Writer.Allocating = .init(allocator); defer str.deinit(); - try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); - const payload = str.written(); + const payload: ?[]const u8 = str.written(); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.PUT, - .extra_headers = headers, - .payload = payload, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.PUT, uri_buf.written(), payload); } ///////////////// @@ -487,26 +824,20 @@ pub fn updateUser(allocator: std.mem.Allocator, io: std.Io, username: []const u8 // Description: // This can only be done by the logged in user. // -pub fn deleteUser(allocator: std.mem.Allocator, io: std.Io, username: []const u8) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn deleteUser(client: *Client, username: []const u8) !void { + var raw = try deleteUserRaw(client, username); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn deleteUserRaw(client: *Client, username: []const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/{s}", .{ client.base_url, username }); + const payload: ?[]const u8 = null; - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/user/{s}", .{username}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.DELETE, - .extra_headers = headers, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.DELETE, uri_buf.written(), payload); } ///////////////// @@ -516,33 +847,24 @@ pub fn deleteUser(allocator: std.mem.Allocator, io: std.Io, username: []const u8 // Description: // This can only be done by the logged in user. // -pub fn createUser(allocator: std.mem.Allocator, io: std.Io, requestBody: User) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn createUser(client: *Client, requestBody: User) !void { + var raw = try createUserRaw(client, requestBody); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn createUserRaw(client: *Client, requestBody: User) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user", .{client.base_url}); - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/user", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); var str: std.Io.Writer.Allocating = .init(allocator); defer str.deinit(); - try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); - const payload = str.written(); + const payload: ?[]const u8 = str.written(); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.POST, - .extra_headers = headers, - .payload = payload, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); } ///////////////// @@ -552,33 +874,37 @@ pub fn createUser(allocator: std.mem.Allocator, io: std.Io, requestBody: User) ! // Description: // Creates list of users with given input array // -pub fn createUsersWithListInput(allocator: std.mem.Allocator, io: std.Io, requestBody: []const u8) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn createUsersWithListInput(client: *Client, requestBody: []const std.json.Value) !Owned(User) { + var result = try createUsersWithListInputResult(client, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn createUsersWithListInputRaw(client: *Client, requestBody: []const std.json.Value) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/createWithList", .{client.base_url}); - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/user/createWithList", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); var str: std.Io.Writer.Allocating = .init(allocator); defer str.deinit(); - try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); - const payload = str.written(); + const payload: ?[]const u8 = str.written(); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.POST, - .extra_headers = headers, - .payload = payload, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +pub fn createUsersWithListInputResult(client: *Client, requestBody: []const std.json.Value) !ApiResult(User) { + return parseRawResponse(User, try createUsersWithListInputRaw(client, requestBody)); } ///////////////// @@ -588,33 +914,37 @@ pub fn createUsersWithListInput(allocator: std.mem.Allocator, io: std.Io, reques // Description: // Add a new pet to the store // -pub fn addPet(allocator: std.mem.Allocator, io: std.Io, requestBody: Pet) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn addPet(client: *Client, requestBody: Pet) !Owned(Pet) { + var result = try addPetResult(client, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn addPetRaw(client: *Client, requestBody: Pet) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet", .{client.base_url}); - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/pet", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); var str: std.Io.Writer.Allocating = .init(allocator); defer str.deinit(); - try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); - const payload = str.written(); + const payload: ?[]const u8 = str.written(); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.POST, - .extra_headers = headers, - .payload = payload, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +pub fn addPetResult(client: *Client, requestBody: Pet) !ApiResult(Pet) { + return parseRawResponse(Pet, try addPetRaw(client, requestBody)); } ///////////////// @@ -624,33 +954,37 @@ pub fn addPet(allocator: std.mem.Allocator, io: std.Io, requestBody: Pet) !void // Description: // Update an existing pet by Id // -pub fn updatePet(allocator: std.mem.Allocator, io: std.Io, requestBody: Pet) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn updatePet(client: *Client, requestBody: Pet) !Owned(Pet) { + var result = try updatePetResult(client, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn updatePetRaw(client: *Client, requestBody: Pet) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet", .{client.base_url}); - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/pet", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); var str: std.Io.Writer.Allocating = .init(allocator); defer str.deinit(); - try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); - const payload = str.written(); + const payload: ?[]const u8 = str.written(); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.PUT, - .extra_headers = headers, - .payload = payload, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.PUT, uri_buf.written(), payload); +} + +pub fn updatePetResult(client: *Client, requestBody: Pet) !ApiResult(Pet) { + return parseRawResponse(Pet, try updatePetRaw(client, requestBody)); } ///////////////// @@ -660,36 +994,33 @@ pub fn updatePet(allocator: std.mem.Allocator, io: std.Io, requestBody: Pet) !vo // Description: // For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions // -pub fn getOrderById(allocator: std.mem.Allocator, io: std.Io, orderId: []const u8) !Order { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); - - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; - - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/store/order/{s}", .{orderId}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - var response_body: std.Io.Writer.Allocating = .init(allocator); - defer response_body.deinit(); - - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, - .response_writer = &response_body.writer, - }); - if (result.status.class() != .success) { - return error.ResponseError; +pub fn getOrderById(client: *Client, orderId: i64) !Owned(Order) { + var result = try getOrderByIdResult(client, orderId); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, } +} - const body = response_body.written(); - const parsed = try std.json.parseFromSlice(Order, allocator, body, .{}); - defer parsed.deinit(); +pub fn getOrderByIdRaw(client: *Client, orderId: i64) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/store/order/{d}", .{ client.base_url, orderId }); + const payload: ?[]const u8 = null; - return parsed.value; + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn getOrderByIdResult(client: *Client, orderId: i64) !ApiResult(Order) { + return parseRawResponse(Order, try getOrderByIdRaw(client, orderId)); } ///////////////// @@ -699,26 +1030,20 @@ pub fn getOrderById(allocator: std.mem.Allocator, io: std.Io, orderId: []const u // Description: // For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors // -pub fn deleteOrder(allocator: std.mem.Allocator, io: std.Io, orderId: []const u8) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn deleteOrder(client: *Client, orderId: i64) !void { + var raw = try deleteOrderRaw(client, orderId); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn deleteOrderRaw(client: *Client, orderId: i64) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/store/order/{d}", .{ client.base_url, orderId }); + const payload: ?[]const u8 = null; - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/store/order/{s}", .{orderId}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.DELETE, - .extra_headers = headers, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.DELETE, uri_buf.written(), payload); } ///////////////// @@ -728,22 +1053,140 @@ pub fn deleteOrder(allocator: std.mem.Allocator, io: std.Io, orderId: []const u8 // Description: // // -pub fn logoutUser(allocator: std.mem.Allocator, io: std.Io) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn logoutUser(client: *Client) !void { + var raw = try logoutUserRaw(client); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn logoutUserRaw(client: *Client) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/logout", .{client.base_url}); + const payload: ?[]const u8 = null; - const uri = try std.Uri.parse("https://petstore3.swagger.io/api/v3/user/logout"); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); } + +pub const resources = struct { + pub const pet = struct { + pub fn addpet(client: *Client, requestBody: Pet) !Owned(Pet) { + return addPet(client, requestBody); + } + pub fn addpetResult(client: *Client, requestBody: Pet) !ApiResult(Pet) { + return addPetResult(client, requestBody); + } + pub fn delete(client: *Client, api_key: []const u8, petId: i64) !void { + return deletePet(client, api_key, petId); + } + pub fn get(client: *Client, petId: i64) !Owned(Pet) { + return getPetById(client, petId); + } + pub fn getResult(client: *Client, petId: i64) !ApiResult(Pet) { + return getPetByIdResult(client, petId); + } + pub fn updatepet_(client: *Client, requestBody: Pet) !Owned(Pet) { + return updatePet(client, requestBody); + } + pub fn updatepet_Result(client: *Client, requestBody: Pet) !ApiResult(Pet) { + return updatePetResult(client, requestBody); + } + pub fn updatepetwithform_(client: *Client, petId: i64, name: ?[]const u8, status: ?[]const u8) !void { + return updatePetWithForm(client, petId, name, status); + } + pub const findbystatus = struct { + pub fn findpetsbystatus(client: *Client, status: ?[]const u8) !Owned([]const std.json.Value) { + return findPetsByStatus(client, status); + } + pub fn findpetsbystatusResult(client: *Client, status: ?[]const u8) !ApiResult([]const std.json.Value) { + return findPetsByStatusResult(client, status); + } + }; + pub const findbytags = struct { + pub fn findpetsbytags(client: *Client, tags: ?[]const u8) !Owned([]const std.json.Value) { + return findPetsByTags(client, tags); + } + pub fn findpetsbytagsResult(client: *Client, tags: ?[]const u8) !ApiResult([]const std.json.Value) { + return findPetsByTagsResult(client, tags); + } + }; + pub const uploadimage = struct { + pub fn uploadfile(client: *Client, petId: i64, additionalMetadata: ?[]const u8, requestBody: []const u8) !Owned(ApiResponse) { + return uploadFile(client, petId, additionalMetadata, requestBody); + } + pub fn uploadfileResult(client: *Client, petId: i64, additionalMetadata: ?[]const u8, requestBody: []const u8) !ApiResult(ApiResponse) { + return uploadFileResult(client, petId, additionalMetadata, requestBody); + } + }; + }; + pub const store = struct { + pub const inventory = struct { + pub fn get(client: *Client) !Owned(std.json.Value) { + return getInventory(client); + } + pub fn getResult(client: *Client) !ApiResult(std.json.Value) { + return getInventoryResult(client); + } + }; + pub const order = struct { + pub fn delete(client: *Client, orderId: i64) !void { + return deleteOrder(client, orderId); + } + pub fn get(client: *Client, orderId: i64) !Owned(Order) { + return getOrderById(client, orderId); + } + pub fn getResult(client: *Client, orderId: i64) !ApiResult(Order) { + return getOrderByIdResult(client, orderId); + } + pub fn placeorder(client: *Client, requestBody: Order) !Owned(Order) { + return placeOrder(client, requestBody); + } + pub fn placeorderResult(client: *Client, requestBody: Order) !ApiResult(Order) { + return placeOrderResult(client, requestBody); + } + }; + }; + pub const user = struct { + pub fn create(client: *Client, requestBody: User) !void { + return createUser(client, requestBody); + } + pub fn delete(client: *Client, username: []const u8) !void { + return deleteUser(client, username); + } + pub fn get(client: *Client, username: []const u8) !Owned(User) { + return getUserByName(client, username); + } + pub fn getResult(client: *Client, username: []const u8) !ApiResult(User) { + return getUserByNameResult(client, username); + } + pub fn update(client: *Client, username: []const u8, requestBody: User) !void { + return updateUser(client, username, requestBody); + } + pub const createwithlist = struct { + pub fn create(client: *Client, requestBody: []const std.json.Value) !Owned(User) { + return createUsersWithListInput(client, requestBody); + } + pub fn createResult(client: *Client, requestBody: []const std.json.Value) !ApiResult(User) { + return createUsersWithListInputResult(client, requestBody); + } + }; + pub const login = struct { + pub fn loginuser(client: *Client, username: ?[]const u8, password: ?[]const u8) !Owned([]const u8) { + return loginUser(client, username, password); + } + pub fn loginuserResult(client: *Client, username: ?[]const u8, password: ?[]const u8) !ApiResult([]const u8) { + return loginUserResult(client, username, password); + } + }; + pub const logout = struct { + pub fn logoutuser(client: *Client) !void { + return logoutUser(client); + } + }; + }; +}; + +pub const pet = resources.pet; +pub const store = resources.store; +pub const user = resources.user; diff --git a/generated/generated_v31.zig b/generated/generated_v31.zig index 2e6b796..e21f58b 100644 --- a/generated/generated_v31.zig +++ b/generated/generated_v31.zig @@ -13,3 +13,360 @@ pub const Pet = struct { /////////////////////////////////////////// // Generated Zig API client from OpenAPI /////////////////////////////////////////// + +pub fn Owned(comptime T: type) type { + return struct { + allocator: std.mem.Allocator, + body: []u8, + parsed: std.json.Parsed(T), + + pub fn deinit(self: *@This()) void { + self.parsed.deinit(); + self.allocator.free(self.body); + } + + pub fn value(self: *@This()) *T { + return &self.parsed.value; + } + }; +} + +pub const RawResponse = struct { + allocator: std.mem.Allocator, + status: std.http.Status, + body: []u8, + + pub fn deinit(self: *@This()) void { + self.allocator.free(self.body); + } +}; + +pub const ParseErrorResponse = struct { + raw: RawResponse, + error_name: []const u8, +}; + +pub fn ApiResult(comptime T: type) type { + return union(enum) { + ok: Owned(T), + api_error: RawResponse, + parse_error: ParseErrorResponse, + + pub fn deinit(self: *@This()) void { + switch (self.*) { + .ok => |*value| value.deinit(), + .api_error => |*value| value.deinit(), + .parse_error => |*value| value.raw.deinit(), + } + } + }; +} + +pub const Client = struct { + allocator: std.mem.Allocator, + io: std.Io, + http: std.http.Client, + api_key: []const u8, + base_url: []const u8 = "", + organization: ?[]const u8 = null, + project: ?[]const u8 = null, + default_headers: []const std.http.Header = &.{}, + + pub fn init(allocator: std.mem.Allocator, io: std.Io, api_key: []const u8) Client { + return .{ + .allocator = allocator, + .io = io, + .http = .{ .allocator = allocator, .io = io }, + .api_key = api_key, + }; + } + + pub fn deinit(self: *Client) void { + self.http.deinit(); + } + + pub fn withBaseUrl(self: *Client, base_url: []const u8) void { + self.base_url = base_url; + } +}; + +fn isQueryChar(c: u8) bool { + return std.ascii.isAlphanumeric(c) or switch (c) { + '-', '.', '_', '~' => true, + else => false, + }; +} + +fn writeQueryComponent(writer: *std.Io.Writer, value: []const u8) !void { + try std.Uri.Component.percentEncode(writer, value, isQueryChar); +} + +fn writeQueryValue(writer: *std.Io.Writer, value: anytype) !void { + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .pointer => |ptr| { + if (ptr.size == .slice and ptr.child == u8) { + try writeQueryComponent(writer, value); + } else { + try std.json.Stringify.value(value, .{}, writer); + } + }, + .int, .comptime_int, .float, .comptime_float, .bool => try writer.print("{}", .{value}), + .@"enum" => try writeQueryComponent(writer, @tagName(value)), + else => try std.json.Stringify.value(value, .{}, writer), + } +} + +fn appendQueryParam(writer: *std.Io.Writer, first_query: *bool, name: []const u8, value: anytype) !void { + if (first_query.*) { + try writer.writeByte('?'); + first_query.* = false; + } else { + try writer.writeByte('&'); + } + try writeQueryComponent(writer, name); + try writer.writeByte('='); + try writeQueryValue(writer, value); +} + +pub fn requestRaw(client: *Client, method: std.http.Method, url: []const u8, payload: ?[]const u8) !RawResponse { + const allocator = client.allocator; + var headers = std.ArrayList(std.http.Header).empty; + defer headers.deinit(allocator); + const auth_header = try appendClientHeaders(allocator, &headers, client, payload != null, "application/json"); + defer if (auth_header) |value| allocator.free(value); + + const uri = try std.Uri.parse(url); + var response_body: std.Io.Writer.Allocating = .init(allocator); + defer response_body.deinit(); + + const result = try client.http.fetch(.{ + .location = .{ .uri = uri }, + .method = method, + .extra_headers = headers.items, + .payload = payload, + .response_writer = &response_body.writer, + }); + + return .{ + .allocator = allocator, + .status = result.status, + .body = try response_body.toOwnedSlice(), + }; +} + +pub fn getRaw(client: *Client, path: []const u8) !RawResponse { + const url = try std.fmt.allocPrint(client.allocator, "{s}{s}", .{ client.base_url, path }); + defer client.allocator.free(url); + return requestRaw(client, .GET, url, null); +} + +pub fn postJsonRaw(client: *Client, path: []const u8, payload: anytype) !RawResponse { + const allocator = client.allocator; + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(payload, .{ .emit_null_optional_fields = false }, &str.writer); + + const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ client.base_url, path }); + defer allocator.free(url); + return requestRaw(client, .POST, url, str.written()); +} + +pub fn parseRawResponse(comptime T: type, raw: RawResponse) !ApiResult(T) { + if (raw.status.class() != .success) return .{ .api_error = raw }; + const parsed = std.json.parseFromSlice(T, raw.allocator, raw.body, .{ .ignore_unknown_fields = true }) catch |err| { + return .{ .parse_error = .{ .raw = raw, .error_name = @errorName(err) } }; + }; + return .{ .ok = .{ .allocator = raw.allocator, .body = raw.body, .parsed = parsed } }; +} + +pub fn getJsonResult(comptime T: type, client: *Client, path: []const u8) !ApiResult(T) { + return parseRawResponse(T, try getRaw(client, path)); +} + +pub fn postJsonResult(comptime T: type, client: *Client, path: []const u8, payload: anytype) !ApiResult(T) { + return parseRawResponse(T, try postJsonRaw(client, path, payload)); +} + +const max_sse_line_size = 256 * 1024; +const max_sse_event_size = 1024 * 1024; + +pub fn parseSseBytes(allocator: std.mem.Allocator, bytes: []const u8, callback: anytype) !void { + var reader: std.Io.Reader = .fixed(bytes); + try parseSseReader(allocator, &reader, callback); +} + +pub fn parseSseReader(allocator: std.mem.Allocator, reader: *std.Io.Reader, callback: anytype) !void { + var line_buf: std.Io.Writer.Allocating = .init(allocator); + defer line_buf.deinit(); + + var event_data: std.Io.Writer.Allocating = .init(allocator); + defer event_data.deinit(); + + while (true) { + line_buf.clearRetainingCapacity(); + + _ = reader.streamDelimiterLimit(&line_buf.writer, '\n', .limited(max_sse_line_size)) catch |err| switch (err) { + error.StreamTooLong => return error.SseLineTooLong, + error.ReadFailed => return err, + error.WriteFailed => return err, + }; + + const ended_with_delimiter = blk: { + const byte = reader.peekByte() catch |err| switch (err) { + error.EndOfStream => break :blk false, + error.ReadFailed => return err, + }; + if (byte == '\n') { + _ = try reader.takeByte(); + break :blk true; + } + break :blk false; + }; + + if (try processSseLine(&event_data, line_buf.written(), callback)) return; + if (!ended_with_delimiter) break; + } + + _ = try dispatchSseEvent(&event_data, callback); +} + +fn processSseLine(event_data: *std.Io.Writer.Allocating, raw_line: []const u8, callback: anytype) !bool { + const line = std.mem.trimEnd(u8, raw_line, "\r"); + if (line.len == 0) return try dispatchSseEvent(event_data, callback); + if (line[0] == ':') return false; + + const colon = std.mem.indexOfScalar(u8, line, ':') orelse return false; + const field = line[0..colon]; + if (!std.mem.eql(u8, field, "data")) return false; + + var value = line[colon + 1 ..]; + if (value.len > 0 and value[0] == ' ') value = value[1..]; + const separator_len: usize = if (event_data.written().len == 0) 0 else 1; + if (event_data.written().len + separator_len + value.len > max_sse_event_size) return error.SseEventTooLong; + if (separator_len != 0) try event_data.writer.writeByte('\n'); + try event_data.writer.writeAll(value); + return false; +} + +fn dispatchSseEvent(event_data: *std.Io.Writer.Allocating, callback: anytype) !bool { + const data = event_data.written(); + if (data.len == 0) return false; + defer event_data.clearRetainingCapacity(); + + if (std.mem.eql(u8, data, "[DONE]")) return true; + try callback.event(data); + return false; +} + +fn TypedSseCallback(comptime T: type, comptime Callback: type) type { + return struct { + allocator: std.mem.Allocator, + callback: *Callback, + + pub fn event(self: *@This(), data: []const u8) !void { + var parsed = try std.json.parseFromSlice(T, self.allocator, data, .{ .ignore_unknown_fields = true }); + defer parsed.deinit(); + try self.callback.event(&parsed.value); + } + }; +} + +pub fn parseSseBytesTyped(comptime T: type, allocator: std.mem.Allocator, bytes: []const u8, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = allocator, .callback = callback }; + try parseSseBytes(allocator, bytes, &typed_callback); +} + +pub fn parseSseReaderTyped(comptime T: type, allocator: std.mem.Allocator, reader: *std.Io.Reader, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = allocator, .callback = callback }; + try parseSseReader(allocator, reader, &typed_callback); +} + +fn stringifyStreamRequest(allocator: std.mem.Allocator, requestBody: anytype) ![]u8 { + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, str.written(), .{ .ignore_unknown_fields = true }); + defer parsed.deinit(); + + if (parsed.value == .object) { + try parsed.value.object.put(parsed.arena.allocator(), "stream", .{ .bool = true }); + } + + var out: std.Io.Writer.Allocating = .init(allocator); + errdefer out.deinit(); + try std.json.Stringify.value(parsed.value, .{ .emit_null_optional_fields = false }, &out.writer); + return try out.toOwnedSlice(); +} + +fn streamJsonTyped(comptime T: type, client: *Client, path: []const u8, requestBody: anytype, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = client.allocator, .callback = callback }; + try streamJson(client, path, requestBody, &typed_callback); +} + +fn streamJson(client: *Client, path: []const u8, requestBody: anytype, callback: anytype) !void { + const allocator = client.allocator; + const payload = try stringifyStreamRequest(allocator, requestBody); + defer allocator.free(payload); + + var headers = std.ArrayList(std.http.Header).empty; + defer headers.deinit(allocator); + const auth_header = try appendClientHeaders(allocator, &headers, client, true, "text/event-stream"); + defer if (auth_header) |value| allocator.free(value); + + const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ client.base_url, path }); + defer allocator.free(url); + const uri = try std.Uri.parse(url); + + var req = try client.http.request(.POST, uri, .{ + .redirect_behavior = .unhandled, + .headers = .{ .accept_encoding = .{ .override = "identity" } }, + .extra_headers = headers.items, + }); + defer req.deinit(); + + req.transfer_encoding = .{ .content_length = payload.len }; + var body = try req.sendBodyUnflushed(&.{}); + try body.writer.writeAll(payload); + try body.end(); + try req.connection.?.flush(); + + var response = try req.receiveHead(&.{}); + if (response.head.status.class() != .success) return error.ResponseError; + + var transfer_buffer: [8 * 1024]u8 = undefined; + const reader = response.reader(&transfer_buffer); + parseSseReader(allocator, reader, callback) catch |err| switch (err) { + error.ReadFailed => return response.bodyErr() orelse err, + else => return err, + }; +} + +fn appendClientHeaders(allocator: std.mem.Allocator, headers: *std.ArrayList(std.http.Header), client: *Client, include_content_type: bool, accept: []const u8) !?[]u8 { + if (include_content_type) { + try headers.append(allocator, .{ .name = "Content-Type", .value = "application/json" }); + } + try headers.append(allocator, .{ .name = "Accept", .value = accept }); + + var auth_header: ?[]u8 = null; + if (client.api_key.len > 0) { + auth_header = try std.fmt.allocPrint(allocator, "Bearer {s}", .{client.api_key}); + try headers.append(allocator, .{ .name = "Authorization", .value = auth_header.? }); + } + if (client.organization) |organization| { + try headers.append(allocator, .{ .name = "OpenAI-Organization", .value = organization }); + } + if (client.project) |project| { + try headers.append(allocator, .{ .name = "OpenAI-Project", .value = project }); + } + for (client.default_headers) |header| { + try headers.append(allocator, header); + } + return auth_header; +} + +pub const resources = struct {}; diff --git a/generated/generated_v32.zig b/generated/generated_v32.zig index 33bf234..c414df7 100644 --- a/generated/generated_v32.zig +++ b/generated/generated_v32.zig @@ -11,7 +11,7 @@ pub const Category = struct { pub const Pet = struct { status: ?[]const u8 = null, - tags: ?[]const std.json.Value = null, + tags: ?[]const Tag = null, category: ?Category = null, id: ?i64 = null, name: []const u8, @@ -53,80 +53,431 @@ pub const ApiResponse = struct { // Generated Zig API client from OpenAPI /////////////////////////////////////////// -///////////////// -// Summary: -// Returns pet inventories by status -// -// Description: -// Returns a map of status codes to quantities -// -pub fn getInventory(allocator: std.mem.Allocator, io: std.Io) !std.json.Value { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn Owned(comptime T: type) type { + return struct { + allocator: std.mem.Allocator, + body: []u8, + parsed: std.json.Parsed(T), + + pub fn deinit(self: *@This()) void { + self.parsed.deinit(); + self.allocator.free(self.body); + } + + pub fn value(self: *@This()) *T { + return &self.parsed.value; + } + }; +} + +pub const RawResponse = struct { + allocator: std.mem.Allocator, + status: std.http.Status, + body: []u8, + + pub fn deinit(self: *@This()) void { + self.allocator.free(self.body); + } +}; + +pub const ParseErrorResponse = struct { + raw: RawResponse, + error_name: []const u8, +}; - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, +pub fn ApiResult(comptime T: type) type { + return union(enum) { + ok: Owned(T), + api_error: RawResponse, + parse_error: ParseErrorResponse, + + pub fn deinit(self: *@This()) void { + switch (self.*) { + .ok => |*value| value.deinit(), + .api_error => |*value| value.deinit(), + .parse_error => |*value| value.raw.deinit(), + } + } }; +} + +pub const Client = struct { + allocator: std.mem.Allocator, + io: std.Io, + http: std.http.Client, + api_key: []const u8, + base_url: []const u8 = "https://petstore3.swagger.io/api/v3", + organization: ?[]const u8 = null, + project: ?[]const u8 = null, + default_headers: []const std.http.Header = &.{}, + + pub fn init(allocator: std.mem.Allocator, io: std.Io, api_key: []const u8) Client { + return .{ + .allocator = allocator, + .io = io, + .http = .{ .allocator = allocator, .io = io }, + .api_key = api_key, + }; + } - const uri = try std.Uri.parse("https://petstore3.swagger.io/api/v3/store/inventory"); + pub fn deinit(self: *Client) void { + self.http.deinit(); + } + + pub fn withBaseUrl(self: *Client, base_url: []const u8) void { + self.base_url = base_url; + } +}; + +fn isQueryChar(c: u8) bool { + return std.ascii.isAlphanumeric(c) or switch (c) { + '-', '.', '_', '~' => true, + else => false, + }; +} + +fn writeQueryComponent(writer: *std.Io.Writer, value: []const u8) !void { + try std.Uri.Component.percentEncode(writer, value, isQueryChar); +} + +fn writeQueryValue(writer: *std.Io.Writer, value: anytype) !void { + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .pointer => |ptr| { + if (ptr.size == .slice and ptr.child == u8) { + try writeQueryComponent(writer, value); + } else { + try std.json.Stringify.value(value, .{}, writer); + } + }, + .int, .comptime_int, .float, .comptime_float, .bool => try writer.print("{}", .{value}), + .@"enum" => try writeQueryComponent(writer, @tagName(value)), + else => try std.json.Stringify.value(value, .{}, writer), + } +} + +fn appendQueryParam(writer: *std.Io.Writer, first_query: *bool, name: []const u8, value: anytype) !void { + if (first_query.*) { + try writer.writeByte('?'); + first_query.* = false; + } else { + try writer.writeByte('&'); + } + try writeQueryComponent(writer, name); + try writer.writeByte('='); + try writeQueryValue(writer, value); +} + +pub fn requestRaw(client: *Client, method: std.http.Method, url: []const u8, payload: ?[]const u8) !RawResponse { + const allocator = client.allocator; + var headers = std.ArrayList(std.http.Header).empty; + defer headers.deinit(allocator); + const auth_header = try appendClientHeaders(allocator, &headers, client, payload != null, "application/json"); + defer if (auth_header) |value| allocator.free(value); + + const uri = try std.Uri.parse(url); var response_body: std.Io.Writer.Allocating = .init(allocator); defer response_body.deinit(); - const result = try client.fetch(.{ + const result = try client.http.fetch(.{ .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, + .method = method, + .extra_headers = headers.items, + .payload = payload, .response_writer = &response_body.writer, }); - if (result.status.class() != .success) { - return error.ResponseError; + + return .{ + .allocator = allocator, + .status = result.status, + .body = try response_body.toOwnedSlice(), + }; +} + +pub fn getRaw(client: *Client, path: []const u8) !RawResponse { + const url = try std.fmt.allocPrint(client.allocator, "{s}{s}", .{ client.base_url, path }); + defer client.allocator.free(url); + return requestRaw(client, .GET, url, null); +} + +pub fn postJsonRaw(client: *Client, path: []const u8, payload: anytype) !RawResponse { + const allocator = client.allocator; + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(payload, .{ .emit_null_optional_fields = false }, &str.writer); + + const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ client.base_url, path }); + defer allocator.free(url); + return requestRaw(client, .POST, url, str.written()); +} + +pub fn parseRawResponse(comptime T: type, raw: RawResponse) !ApiResult(T) { + if (raw.status.class() != .success) return .{ .api_error = raw }; + const parsed = std.json.parseFromSlice(T, raw.allocator, raw.body, .{ .ignore_unknown_fields = true }) catch |err| { + return .{ .parse_error = .{ .raw = raw, .error_name = @errorName(err) } }; + }; + return .{ .ok = .{ .allocator = raw.allocator, .body = raw.body, .parsed = parsed } }; +} + +pub fn getJsonResult(comptime T: type, client: *Client, path: []const u8) !ApiResult(T) { + return parseRawResponse(T, try getRaw(client, path)); +} + +pub fn postJsonResult(comptime T: type, client: *Client, path: []const u8, payload: anytype) !ApiResult(T) { + return parseRawResponse(T, try postJsonRaw(client, path, payload)); +} + +const max_sse_line_size = 256 * 1024; +const max_sse_event_size = 1024 * 1024; + +pub fn parseSseBytes(allocator: std.mem.Allocator, bytes: []const u8, callback: anytype) !void { + var reader: std.Io.Reader = .fixed(bytes); + try parseSseReader(allocator, &reader, callback); +} + +pub fn parseSseReader(allocator: std.mem.Allocator, reader: *std.Io.Reader, callback: anytype) !void { + var line_buf: std.Io.Writer.Allocating = .init(allocator); + defer line_buf.deinit(); + + var event_data: std.Io.Writer.Allocating = .init(allocator); + defer event_data.deinit(); + + while (true) { + line_buf.clearRetainingCapacity(); + + _ = reader.streamDelimiterLimit(&line_buf.writer, '\n', .limited(max_sse_line_size)) catch |err| switch (err) { + error.StreamTooLong => return error.SseLineTooLong, + error.ReadFailed => return err, + error.WriteFailed => return err, + }; + + const ended_with_delimiter = blk: { + const byte = reader.peekByte() catch |err| switch (err) { + error.EndOfStream => break :blk false, + error.ReadFailed => return err, + }; + if (byte == '\n') { + _ = try reader.takeByte(); + break :blk true; + } + break :blk false; + }; + + if (try processSseLine(&event_data, line_buf.written(), callback)) return; + if (!ended_with_delimiter) break; } - const body = response_body.written(); - const parsed = try std.json.parseFromSlice(std.json.Value, allocator, body, .{}); + _ = try dispatchSseEvent(&event_data, callback); +} + +fn processSseLine(event_data: *std.Io.Writer.Allocating, raw_line: []const u8, callback: anytype) !bool { + const line = std.mem.trimEnd(u8, raw_line, "\r"); + if (line.len == 0) return try dispatchSseEvent(event_data, callback); + if (line[0] == ':') return false; + + const colon = std.mem.indexOfScalar(u8, line, ':') orelse return false; + const field = line[0..colon]; + if (!std.mem.eql(u8, field, "data")) return false; + + var value = line[colon + 1 ..]; + if (value.len > 0 and value[0] == ' ') value = value[1..]; + const separator_len: usize = if (event_data.written().len == 0) 0 else 1; + if (event_data.written().len + separator_len + value.len > max_sse_event_size) return error.SseEventTooLong; + if (separator_len != 0) try event_data.writer.writeByte('\n'); + try event_data.writer.writeAll(value); + return false; +} + +fn dispatchSseEvent(event_data: *std.Io.Writer.Allocating, callback: anytype) !bool { + const data = event_data.written(); + if (data.len == 0) return false; + defer event_data.clearRetainingCapacity(); + + if (std.mem.eql(u8, data, "[DONE]")) return true; + try callback.event(data); + return false; +} + +fn TypedSseCallback(comptime T: type, comptime Callback: type) type { + return struct { + allocator: std.mem.Allocator, + callback: *Callback, + + pub fn event(self: *@This(), data: []const u8) !void { + var parsed = try std.json.parseFromSlice(T, self.allocator, data, .{ .ignore_unknown_fields = true }); + defer parsed.deinit(); + try self.callback.event(&parsed.value); + } + }; +} + +pub fn parseSseBytesTyped(comptime T: type, allocator: std.mem.Allocator, bytes: []const u8, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = allocator, .callback = callback }; + try parseSseBytes(allocator, bytes, &typed_callback); +} + +pub fn parseSseReaderTyped(comptime T: type, allocator: std.mem.Allocator, reader: *std.Io.Reader, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = allocator, .callback = callback }; + try parseSseReader(allocator, reader, &typed_callback); +} + +fn stringifyStreamRequest(allocator: std.mem.Allocator, requestBody: anytype) ![]u8 { + var str: std.Io.Writer.Allocating = .init(allocator); + defer str.deinit(); + try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, str.written(), .{ .ignore_unknown_fields = true }); defer parsed.deinit(); - return parsed.value; + if (parsed.value == .object) { + try parsed.value.object.put(parsed.arena.allocator(), "stream", .{ .bool = true }); + } + + var out: std.Io.Writer.Allocating = .init(allocator); + errdefer out.deinit(); + try std.json.Stringify.value(parsed.value, .{ .emit_null_optional_fields = false }, &out.writer); + return try out.toOwnedSlice(); +} + +fn streamJsonTyped(comptime T: type, client: *Client, path: []const u8, requestBody: anytype, callback: anytype) !void { + const Callback = @TypeOf(callback.*); + var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = client.allocator, .callback = callback }; + try streamJson(client, path, requestBody, &typed_callback); +} + +fn streamJson(client: *Client, path: []const u8, requestBody: anytype, callback: anytype) !void { + const allocator = client.allocator; + const payload = try stringifyStreamRequest(allocator, requestBody); + defer allocator.free(payload); + + var headers = std.ArrayList(std.http.Header).empty; + defer headers.deinit(allocator); + const auth_header = try appendClientHeaders(allocator, &headers, client, true, "text/event-stream"); + defer if (auth_header) |value| allocator.free(value); + + const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ client.base_url, path }); + defer allocator.free(url); + const uri = try std.Uri.parse(url); + + var req = try client.http.request(.POST, uri, .{ + .redirect_behavior = .unhandled, + .headers = .{ .accept_encoding = .{ .override = "identity" } }, + .extra_headers = headers.items, + }); + defer req.deinit(); + + req.transfer_encoding = .{ .content_length = payload.len }; + var body = try req.sendBodyUnflushed(&.{}); + try body.writer.writeAll(payload); + try body.end(); + try req.connection.?.flush(); + + var response = try req.receiveHead(&.{}); + if (response.head.status.class() != .success) return error.ResponseError; + + var transfer_buffer: [8 * 1024]u8 = undefined; + const reader = response.reader(&transfer_buffer); + parseSseReader(allocator, reader, callback) catch |err| switch (err) { + error.ReadFailed => return response.bodyErr() orelse err, + else => return err, + }; +} + +fn appendClientHeaders(allocator: std.mem.Allocator, headers: *std.ArrayList(std.http.Header), client: *Client, include_content_type: bool, accept: []const u8) !?[]u8 { + if (include_content_type) { + try headers.append(allocator, .{ .name = "Content-Type", .value = "application/json" }); + } + try headers.append(allocator, .{ .name = "Accept", .value = accept }); + + var auth_header: ?[]u8 = null; + if (client.api_key.len > 0) { + auth_header = try std.fmt.allocPrint(allocator, "Bearer {s}", .{client.api_key}); + try headers.append(allocator, .{ .name = "Authorization", .value = auth_header.? }); + } + if (client.organization) |organization| { + try headers.append(allocator, .{ .name = "OpenAI-Organization", .value = organization }); + } + if (client.project) |project| { + try headers.append(allocator, .{ .name = "OpenAI-Project", .value = project }); + } + for (client.default_headers) |header| { + try headers.append(allocator, header); + } + return auth_header; } ///////////////// // Summary: -// Get user by user name +// Returns pet inventories by status // // Description: +// Returns a map of status codes to quantities // -// -pub fn getUserByName(allocator: std.mem.Allocator, io: std.Io, username: []const u8) !User { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn getInventory(client: *Client) !Owned(std.json.Value) { + var result = try getInventoryResult(client); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn getInventoryRaw(client: *Client) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/store/inventory", .{client.base_url}); + const payload: ?[]const u8 = null; - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/user/{s}", .{username}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - var response_body: std.Io.Writer.Allocating = .init(allocator); - defer response_body.deinit(); + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, - .response_writer = &response_body.writer, - }); - if (result.status.class() != .success) { - return error.ResponseError; +pub fn getInventoryResult(client: *Client) !ApiResult(std.json.Value) { + return parseRawResponse(std.json.Value, try getInventoryRaw(client)); +} + +///////////////// +// Summary: +// Get user by user name +// +// Description: +// +// +pub fn getUserByName(client: *Client, username: []const u8) !Owned(User) { + var result = try getUserByNameResult(client, username); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, } +} - const body = response_body.written(); - const parsed = try std.json.parseFromSlice(User, allocator, body, .{}); - defer parsed.deinit(); +pub fn getUserByNameRaw(client: *Client, username: []const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user/{s}", .{ client.base_url, username }); + const payload: ?[]const u8 = null; + + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} - return parsed.value; +pub fn getUserByNameResult(client: *Client, username: []const u8) !ApiResult(User) { + return parseRawResponse(User, try getUserByNameRaw(client, username)); } ///////////////// @@ -136,33 +487,37 @@ pub fn getUserByName(allocator: std.mem.Allocator, io: std.Io, username: []const // Description: // Place a new order in the store // -pub fn placeOrder(allocator: std.mem.Allocator, io: std.Io, requestBody: Order) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn placeOrder(client: *Client, requestBody: Order) !Owned(Order) { + var result = try placeOrderResult(client, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn placeOrderRaw(client: *Client, requestBody: Order) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/store/order", .{client.base_url}); - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/store/order", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); var str: std.Io.Writer.Allocating = .init(allocator); defer str.deinit(); - try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); - const payload = str.written(); + const payload: ?[]const u8 = str.written(); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.POST, - .extra_headers = headers, - .payload = payload, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +pub fn placeOrderResult(client: *Client, requestBody: Order) !ApiResult(Order) { + return parseRawResponse(Order, try placeOrderRaw(client, requestBody)); } ///////////////// @@ -172,33 +527,37 @@ pub fn placeOrder(allocator: std.mem.Allocator, io: std.Io, requestBody: Order) // Description: // This can only be done by the logged in user. // -pub fn createUser(allocator: std.mem.Allocator, io: std.Io, requestBody: User) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn createUser(client: *Client, requestBody: User) !Owned(User) { + var result = try createUserResult(client, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn createUserRaw(client: *Client, requestBody: User) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/user", .{client.base_url}); - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/user", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); var str: std.Io.Writer.Allocating = .init(allocator); defer str.deinit(); - try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); - const payload = str.written(); + const payload: ?[]const u8 = str.written(); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.POST, - .extra_headers = headers, - .payload = payload, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +pub fn createUserResult(client: *Client, requestBody: User) !ApiResult(User) { + return parseRawResponse(User, try createUserRaw(client, requestBody)); } ///////////////// @@ -208,36 +567,33 @@ pub fn createUser(allocator: std.mem.Allocator, io: std.Io, requestBody: User) ! // Description: // Returns a single pet // -pub fn getPetById(allocator: std.mem.Allocator, io: std.Io, petId: []const u8) !Pet { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); - - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; - - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/pet/{s}", .{petId}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - var response_body: std.Io.Writer.Allocating = .init(allocator); - defer response_body.deinit(); - - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, - .response_writer = &response_body.writer, - }); - if (result.status.class() != .success) { - return error.ResponseError; +pub fn getPetById(client: *Client, petId: i64) !Owned(Pet) { + var result = try getPetByIdResult(client, petId); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, } +} - const body = response_body.written(); - const parsed = try std.json.parseFromSlice(Pet, allocator, body, .{}); - defer parsed.deinit(); +pub fn getPetByIdRaw(client: *Client, petId: i64) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/{d}", .{ client.base_url, petId }); + const payload: ?[]const u8 = null; - return parsed.value; + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} + +pub fn getPetByIdResult(client: *Client, petId: i64) !ApiResult(Pet) { + return parseRawResponse(Pet, try getPetByIdRaw(client, petId)); } ///////////////// @@ -247,29 +603,21 @@ pub fn getPetById(allocator: std.mem.Allocator, io: std.Io, petId: []const u8) ! // Description: // // -pub fn deletePet(allocator: std.mem.Allocator, io: std.Io, api_key: []const u8, petId: []const u8) !void { - _ = api_key; - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn deletePet(client: *Client, api_key: []const u8, petId: i64) !void { + var raw = try deletePetRaw(client, api_key, petId); + defer raw.deinit(); + if (raw.status.class() != .success) return error.ResponseError; +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn deletePetRaw(client: *Client, api_key: []const u8, petId: i64) !RawResponse { + const allocator = client.allocator; + _ = api_key; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/{d}", .{ client.base_url, petId }); + const payload: ?[]const u8 = null; - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/pet/{s}", .{ - petId, - }); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.DELETE, - .extra_headers = headers, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.DELETE, uri_buf.written(), payload); } ///////////////// @@ -279,33 +627,37 @@ pub fn deletePet(allocator: std.mem.Allocator, io: std.Io, api_key: []const u8, // Description: // Add a new pet to the store // -pub fn addPet(allocator: std.mem.Allocator, io: std.Io, requestBody: Pet) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn addPet(client: *Client, requestBody: Pet) !Owned(Pet) { + var result = try addPetResult(client, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn addPetRaw(client: *Client, requestBody: Pet) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet", .{client.base_url}); - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/pet", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); var str: std.Io.Writer.Allocating = .init(allocator); defer str.deinit(); - try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); - const payload = str.written(); + const payload: ?[]const u8 = str.written(); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.POST, - .extra_headers = headers, - .payload = payload, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.POST, uri_buf.written(), payload); +} + +pub fn addPetResult(client: *Client, requestBody: Pet) !ApiResult(Pet) { + return parseRawResponse(Pet, try addPetRaw(client, requestBody)); } ///////////////// @@ -315,33 +667,37 @@ pub fn addPet(allocator: std.mem.Allocator, io: std.Io, requestBody: Pet) !void // Description: // Update an existing pet by Id // -pub fn updatePet(allocator: std.mem.Allocator, io: std.Io, requestBody: Pet) !void { - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); +pub fn updatePet(client: *Client, requestBody: Pet) !Owned(Pet) { + var result = try updatePetResult(client, requestBody); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; +pub fn updatePetRaw(client: *Client, requestBody: Pet) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet", .{client.base_url}); - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/pet", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); var str: std.Io.Writer.Allocating = .init(allocator); defer str.deinit(); - try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); - const payload = str.written(); + const payload: ?[]const u8 = str.written(); - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.PUT, - .extra_headers = headers, - .payload = payload, - }); - if (result.status.class() != .success) { - return error.ResponseError; - } + return requestRaw(client, std.http.Method.PUT, uri_buf.written(), payload); +} + +pub fn updatePetResult(client: *Client, requestBody: Pet) !ApiResult(Pet) { + return parseRawResponse(Pet, try updatePetRaw(client, requestBody)); } ///////////////// @@ -351,35 +707,105 @@ pub fn updatePet(allocator: std.mem.Allocator, io: std.Io, requestBody: Pet) !vo // Description: // Multiple status values can be provided with comma separated strings // -pub fn findPetsByStatus(allocator: std.mem.Allocator, io: std.Io, status: []const u8) ![]const u8 { - _ = status; - var client: std.http.Client = .{ .allocator = allocator, .io = io }; - defer client.deinit(); - - const headers = &[_]std.http.Header{ - .{ .name = "Content-Type", .value = "application/json" }, - .{ .name = "Accept", .value = "application/json" }, - }; - - const uri_str = try std.fmt.allocPrint(allocator, "https://petstore3.swagger.io/api/v3/pet/findByStatus", .{}); - defer allocator.free(uri_str); - const uri = try std.Uri.parse(uri_str); - var response_body: std.Io.Writer.Allocating = .init(allocator); - defer response_body.deinit(); +pub fn findPetsByStatus(client: *Client, status: ?[]const u8) !Owned([]const std.json.Value) { + var result = try findPetsByStatusResult(client, status); + switch (result) { + .ok => |ok| return ok, + .api_error => |*err| { + err.deinit(); + return error.ResponseError; + }, + .parse_error => |*err| { + err.raw.deinit(); + return error.ResponseParseError; + }, + } +} - const result = try client.fetch(.{ - .location = .{ .uri = uri }, - .method = std.http.Method.GET, - .extra_headers = headers, - .response_writer = &response_body.writer, - }); - if (result.status.class() != .success) { - return error.ResponseError; +pub fn findPetsByStatusRaw(client: *Client, status: ?[]const u8) !RawResponse { + const allocator = client.allocator; + var uri_buf: std.Io.Writer.Allocating = .init(allocator); + defer uri_buf.deinit(); + try uri_buf.writer.print("{s}/pet/findByStatus", .{client.base_url}); + var first_query = true; + if (status) |value| { + try appendQueryParam(&uri_buf.writer, &first_query, "status", value); } + const payload: ?[]const u8 = null; - const body = response_body.written(); - const parsed = try std.json.parseFromSlice([]const u8, allocator, body, .{}); - defer parsed.deinit(); + return requestRaw(client, std.http.Method.GET, uri_buf.written(), payload); +} - return parsed.value; +pub fn findPetsByStatusResult(client: *Client, status: ?[]const u8) !ApiResult([]const std.json.Value) { + return parseRawResponse([]const std.json.Value, try findPetsByStatusRaw(client, status)); } + +pub const resources = struct { + pub const pet = struct { + pub fn addpet(client: *Client, requestBody: Pet) !Owned(Pet) { + return addPet(client, requestBody); + } + pub fn addpetResult(client: *Client, requestBody: Pet) !ApiResult(Pet) { + return addPetResult(client, requestBody); + } + pub fn delete(client: *Client, api_key: []const u8, petId: i64) !void { + return deletePet(client, api_key, petId); + } + pub fn get(client: *Client, petId: i64) !Owned(Pet) { + return getPetById(client, petId); + } + pub fn getResult(client: *Client, petId: i64) !ApiResult(Pet) { + return getPetByIdResult(client, petId); + } + pub fn update(client: *Client, requestBody: Pet) !Owned(Pet) { + return updatePet(client, requestBody); + } + pub fn updateResult(client: *Client, requestBody: Pet) !ApiResult(Pet) { + return updatePetResult(client, requestBody); + } + pub const findbystatus = struct { + pub fn findpetsbystatus(client: *Client, status: ?[]const u8) !Owned([]const std.json.Value) { + return findPetsByStatus(client, status); + } + pub fn findpetsbystatusResult(client: *Client, status: ?[]const u8) !ApiResult([]const std.json.Value) { + return findPetsByStatusResult(client, status); + } + }; + }; + pub const store = struct { + pub const inventory = struct { + pub fn get(client: *Client) !Owned(std.json.Value) { + return getInventory(client); + } + pub fn getResult(client: *Client) !ApiResult(std.json.Value) { + return getInventoryResult(client); + } + }; + pub const order = struct { + pub fn placeorder(client: *Client, requestBody: Order) !Owned(Order) { + return placeOrder(client, requestBody); + } + pub fn placeorderResult(client: *Client, requestBody: Order) !ApiResult(Order) { + return placeOrderResult(client, requestBody); + } + }; + }; + pub const user = struct { + pub fn create(client: *Client, requestBody: User) !Owned(User) { + return createUser(client, requestBody); + } + pub fn createResult(client: *Client, requestBody: User) !ApiResult(User) { + return createUserResult(client, requestBody); + } + pub fn get(client: *Client, username: []const u8) !Owned(User) { + return getUserByName(client, username); + } + pub fn getResult(client: *Client, username: []const u8) !ApiResult(User) { + return getUserByNameResult(client, username); + } + }; +}; + +pub const pet = resources.pet; +pub const store = resources.store; +pub const user = resources.user; diff --git a/generated/main.zig b/generated/main.zig index d1588f3..2106ddf 100644 --- a/generated/main.zig +++ b/generated/main.zig @@ -6,18 +6,25 @@ pub fn main(init: std.process.Init) !void { const allocator = init.gpa; const io = init.io; + var v3_client = v3.Client.init(allocator, io, ""); + defer v3_client.deinit(); + var v2_client = v2.Client.init(allocator, io, ""); + defer v2_client.deinit(); + std.debug.print("Generated models build and run !!\n", .{}); std.debug.print("Testing memory management in generated functions...\n", .{}); - if (v3.getPetById(allocator, io, "1")) |pet3| { - std.debug.print("Found Pet v3 with ID:{any}\n\n", .{pet3.id}); - } else |err| { + var pet3 = v3.getPetById(&v3_client, 1) catch |err| { std.debug.print("Failed to get Pet v3: {any}\n", .{err}); - } + return; + }; + defer pet3.deinit(); + std.debug.print("Found Pet v3 with ID:{any}\n\n", .{pet3.value().id}); - if (v2.getPetById(allocator, io, 1)) |pet2| { - std.debug.print("Found Pet v2 with ID:{any}\n\n", .{pet2.id}); - } else |err| { + var pet2 = v2.getPetById(&v2_client, 1) catch |err| { std.debug.print("Failed to get Pet v2: {any}\n", .{err}); - } + return; + }; + defer pet2.deinit(); + std.debug.print("Found Pet v2 with ID:{any}\n\n", .{pet2.value().id}); } diff --git a/src/cli.zig b/src/cli.zig index 1c8d2c7..fcc11d4 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -1,10 +1,18 @@ const std = @import("std"); const version_info = @import("build_info"); +pub const ResourceWrapperMode = enum { + none, + tags, + paths, + hybrid, +}; + pub const CliArgs = struct { input_path: []const u8, output_path: ?[]const u8 = null, base_url: ?[]const u8 = null, + resource_wrappers: ResourceWrapperMode = .paths, }; pub const ParsedArgs = struct { @@ -27,6 +35,7 @@ pub fn parse(args: []const [:0]const u8) !ParsedArgs { var input_path: ?[]const u8 = null; var output_path: ?[]const u8 = null; var base_url: ?[]const u8 = null; + var resource_wrappers: ResourceWrapperMode = .paths; var i: usize = 2; while (i < args.len) : (i += 1) { @@ -56,6 +65,18 @@ pub fn parse(args: []const [:0]const u8) !ParsedArgs { return error.InvalidArguments; } base_url = args[i]; + } else if (std.mem.eql(u8, arg, "--resource-wrappers")) { + i += 1; + if (i >= args.len) { + printUsage(); + std.debug.print("\nError: resource wrapper mode required\n", .{}); + return error.InvalidArguments; + } + resource_wrappers = parseResourceWrapperMode(args[i]) orelse { + printUsage(); + std.debug.print("\nError: invalid resource wrapper mode '{s}'\n", .{args[i]}); + return error.InvalidArguments; + }; } } @@ -70,10 +91,19 @@ pub fn parse(args: []const [:0]const u8) !ParsedArgs { .input_path = input_path.?, .output_path = output_path, .base_url = base_url, + .resource_wrappers = resource_wrappers, }, }; } +fn parseResourceWrapperMode(value: []const u8) ?ResourceWrapperMode { + if (std.mem.eql(u8, value, "none")) return .none; + if (std.mem.eql(u8, value, "tags")) return .tags; + if (std.mem.eql(u8, value, "paths")) return .paths; + if (std.mem.eql(u8, value, "hybrid")) return .hybrid; + return null; +} + fn printUsage() void { std.debug.print( \\ @@ -86,6 +116,8 @@ fn printUsage() void { \\ (default: generated.zig) \\ --base-url Base URL for the API client. \\ (default: server URL from OpenAPI Specification) + \\ --resource-wrappers Generate resource wrappers: none, tags, paths, hybrid. + \\ (default: paths) \\ \\ EXAMPLES: \\ openapi2zig generate -i ./openapi/petstore.json -o api.zig diff --git a/src/generators/converters/openapi31_converter.zig b/src/generators/converters/openapi31_converter.zig index c50b870..7696467 100644 --- a/src/generators/converters/openapi31_converter.zig +++ b/src/generators/converters/openapi31_converter.zig @@ -37,6 +37,7 @@ const Paths31 = @import("../../models/v3.1/paths.zig").Paths; pub const OpenApi31Converter = struct { allocator: std.mem.Allocator, + source_schemas: ?*const std.StringHashMap(SchemaOrReference31) = null, pub fn init(allocator: std.mem.Allocator) OpenApi31Converter { return OpenApi31Converter{ .allocator = allocator }; @@ -146,6 +147,10 @@ pub const OpenApi31Converter = struct { fn convertSchemas(self: *OpenApi31Converter, components: Components31) !std.StringHashMap(Schema) { var schemas = std.StringHashMap(Schema).init(self.allocator); if (components.schemas) |schemas_map| { + const previous_source_schemas = self.source_schemas; + self.source_schemas = &schemas_map; + defer self.source_schemas = previous_source_schemas; + var schema_iterator = schemas_map.iterator(); while (schema_iterator.next()) |entry| { const key = try self.allocator.dupe(u8, entry.key_ptr.*); @@ -168,7 +173,246 @@ pub const OpenApi31Converter = struct { } } + fn refName(ref: []const u8) []const u8 { + if (std.mem.lastIndexOf(u8, ref, "/")) |last_slash| { + return ref[last_slash + 1 ..]; + } + return ref; + } + + fn convertResolvedSchemaReference(self: *OpenApi31Converter, ref: []const u8) anyerror!?Schema { + const source_schemas = self.source_schemas orelse return null; + const schema_or_ref = source_schemas.get(refName(ref)) orelse return null; + return try self.convertSchemaOrReference(schema_or_ref); + } + + fn mergeRequired(self: *OpenApi31Converter, required_list: *std.ArrayList([]const u8), required: ?[][]const u8) !void { + if (required) |items| { + for (items) |item| { + var exists = false; + for (required_list.items) |existing| { + if (std.mem.eql(u8, existing, item)) { + exists = true; + break; + } + } + if (!exists) try required_list.append(self.allocator, item); + } + } + } + + fn cloneSchema(self: *OpenApi31Converter, schema: Schema) anyerror!Schema { + const required = if (schema.required) |items| blk: { + const cloned = try self.allocator.alloc([]const u8, items.len); + @memcpy(cloned, items); + break :blk cloned; + } else null; + + const properties = if (schema.properties) |props| blk: { + var cloned_props = std.StringHashMap(Schema).init(self.allocator); + var iterator = props.iterator(); + while (iterator.next()) |entry| { + const key = try self.allocator.dupe(u8, entry.key_ptr.*); + try cloned_props.put(key, try self.cloneSchema(entry.value_ptr.*)); + } + break :blk cloned_props; + } else null; + + const items = if (schema.items) |item| blk: { + const cloned_item = try self.allocator.create(Schema); + cloned_item.* = try self.cloneSchema(item.*); + break :blk cloned_item; + } else null; + + const one_of_refs = if (schema.one_of_refs) |refs| try self.cloneStringList(refs) else null; + const any_of_refs = if (schema.any_of_refs) |refs| try self.cloneStringList(refs) else null; + const discriminator_property = if (schema.discriminator_property) |property| try self.allocator.dupe(u8, property) else null; + const one_of = if (schema.one_of) |variants| try self.cloneSchemaList(variants) else null; + const any_of = if (schema.any_of) |variants| try self.cloneSchemaList(variants) else null; + + return Schema{ + .type = schema.type, + .ref = schema.ref, + .title = schema.title, + .description = schema.description, + .format = schema.format, + .required = required, + .properties = properties, + .items = items, + .enum_values = schema.enum_values, + .default = schema.default, + .example = schema.example, + .one_of_refs = one_of_refs, + .any_of_refs = any_of_refs, + .discriminator_property = discriminator_property, + .one_of = one_of, + .any_of = any_of, + }; + } + + fn cloneSchemaList(self: *OpenApi31Converter, values: []const Schema) anyerror![]Schema { + const cloned = try self.allocator.alloc(Schema, values.len); + errdefer self.allocator.free(cloned); + for (values, 0..) |value, i| cloned[i] = try self.cloneSchema(value); + return cloned; + } + + fn cloneStringList(self: *OpenApi31Converter, values: []const []const u8) ![][]const u8 { + const cloned = try self.allocator.alloc([]const u8, values.len); + errdefer self.allocator.free(cloned); + for (values, 0..) |value, i| cloned[i] = try self.allocator.dupe(u8, value); + return cloned; + } + + fn mergeProperties(self: *OpenApi31Converter, merged: *std.StringHashMap(Schema), part: Schema) !void { + if (part.properties) |props| { + var iterator = props.iterator(); + while (iterator.next()) |entry| { + const cloned = try self.cloneSchema(entry.value_ptr.*); + if (merged.getEntry(entry.key_ptr.*)) |existing| { + existing.value_ptr.deinit(self.allocator); + existing.value_ptr.* = cloned; + } else { + const key = try self.allocator.dupe(u8, entry.key_ptr.*); + try merged.put(key, cloned); + } + } + } + } + + fn convertAllOfSchema(self: *OpenApi31Converter, schema: Schema31) anyerror!Schema { + var merged_properties = std.StringHashMap(Schema).init(self.allocator); + var required_list = std.ArrayList([]const u8).empty; + + if (schema.allOf) |all_of| { + for (all_of) |item| { + var converted = switch (item) { + .reference => |ref| (try self.convertResolvedSchemaReference(ref.ref)) orelse Schema{ .type = .reference, .ref = ref.ref }, + .schema => |child| try self.convertSchema(child.*), + }; + try self.mergeProperties(&merged_properties, converted); + try self.mergeRequired(&required_list, converted.required); + converted.deinit(self.allocator); + } + } + + if (schema.properties) |props| { + var prop_iterator = props.iterator(); + while (prop_iterator.next()) |entry| { + const prop_schema = try self.convertSchemaOrReference(entry.value_ptr.*); + if (merged_properties.getEntry(entry.key_ptr.*)) |existing| { + existing.value_ptr.deinit(self.allocator); + existing.value_ptr.* = prop_schema; + } else { + const key = try self.allocator.dupe(u8, entry.key_ptr.*); + try merged_properties.put(key, prop_schema); + } + } + } + + if (schema.required) |required| { + for (required) |item| try required_list.append(self.allocator, item); + } + + const required = if (required_list.items.len > 0) try required_list.toOwnedSlice(self.allocator) else null; + const has_properties = merged_properties.count() > 0; + if (!has_properties) { + merged_properties.deinit(); + } + + return Schema{ + .type = .object, + .ref = null, + .title = schema.title, + .description = schema.description, + .format = schema.format, + .required = required, + .properties = if (has_properties) merged_properties else null, + .items = null, + .enum_values = schema.enum_values, + .default = schema.default, + .example = schema.example, + }; + } + + fn convertUnionVariants(self: *OpenApi31Converter, variants: []const SchemaOrReference31) ![]Schema { + const converted = try self.allocator.alloc(Schema, variants.len); + errdefer self.allocator.free(converted); + for (variants, 0..) |variant, i| { + converted[i] = try self.convertSchemaOrReference(variant); + } + return converted; + } + + fn convertUnionRefs(self: *OpenApi31Converter, variants: []const SchemaOrReference31) !?[][]const u8 { + var refs = try self.allocator.alloc([]const u8, variants.len); + errdefer { + for (refs[0..]) |ref| if (ref.len != 0) self.allocator.free(ref); + self.allocator.free(refs); + } + for (refs) |*ref| ref.* = ""; + + for (variants, 0..) |variant, i| { + switch (variant) { + .reference => |reference| refs[i] = try self.allocator.dupe(u8, refName(reference.ref)), + .schema => { + for (refs) |ref| if (ref.len != 0) self.allocator.free(ref); + self.allocator.free(refs); + return null; + }, + } + } + return refs; + } + + fn convertUnionSchema(self: *OpenApi31Converter, schema: Schema31) anyerror!Schema { + const discriminator = schema.discriminator; + + if (schema.oneOf) |one_of| { + const variants = try self.convertUnionVariants(one_of); + if (discriminator) |disc| { + if (try self.convertUnionRefs(one_of)) |refs| { + return Schema{ + .type = .object, + .one_of_refs = refs, + .discriminator_property = try self.allocator.dupe(u8, disc.propertyName), + .one_of = variants, + }; + } + } + return Schema{ .type = .object, .one_of = variants }; + } + + if (schema.anyOf) |any_of| { + const variants = try self.convertUnionVariants(any_of); + if (discriminator) |disc| { + if (try self.convertUnionRefs(any_of)) |refs| { + return Schema{ + .type = .object, + .any_of_refs = refs, + .discriminator_property = try self.allocator.dupe(u8, disc.propertyName), + .any_of = variants, + }; + } + } + return Schema{ .type = .object, .any_of = variants }; + } + + return Schema{ + .type = .object, + .description = "OpenAPI oneOf with discriminator could not be generated safely; generator currently uses std.json.Value.", + }; + } + fn convertSchema(self: *OpenApi31Converter, schema: Schema31) anyerror!Schema { + if (schema.allOf != null) { + return try self.convertAllOfSchema(schema); + } + + if ((schema.oneOf != null or schema.anyOf != null) and schema.properties == null) { + return try self.convertUnionSchema(schema); + } + // Handle type_array (e.g. ["string", "null"]) by using the first non-null type const schema_type = blk: { if (schema.type) |type_str| { @@ -218,7 +462,7 @@ pub const OpenApi31Converter = struct { .required = required, .properties = properties, .items = items, - .enum_values = null, + .enum_values = schema.enum_values, .default = schema.default, .example = schema.example, }; @@ -232,6 +476,7 @@ pub const OpenApi31Converter = struct { if (std.mem.eql(u8, type_str, "boolean")) return .boolean; if (std.mem.eql(u8, type_str, "array")) return .array; if (std.mem.eql(u8, type_str, "object")) return .object; + if (std.mem.eql(u8, type_str, "null")) return .null; return .string; } @@ -341,7 +586,11 @@ pub const OpenApi31Converter = struct { fn convertRequestBody(self: *OpenApi31Converter, requestBody: *const RequestBody31) !Parameter { var mut_request_body = requestBody.*; var schema: ?Schema = null; - if (mut_request_body.content.count() > 0) { + if (mut_request_body.content.get("application/json")) |media_type| { + if (media_type.schema) |schema_or_ref| { + schema = try self.convertSchemaOrReference(schema_or_ref); + } + } else if (mut_request_body.content.count() > 0) { var it = mut_request_body.content.iterator(); if (it.next()) |entry| { if (entry.value_ptr.schema) |schema_or_ref| { @@ -426,11 +675,17 @@ pub const OpenApi31Converter = struct { const description = response.description; var schema: ?Schema = null; if (response.content) |content| { - var content_iterator = content.iterator(); - if (content_iterator.next()) |entry| { - if (entry.value_ptr.schema) |schema_or_ref| { + if (content.get("application/json")) |media_type| { + if (media_type.schema) |schema_or_ref| { schema = try self.convertSchemaOrReference(schema_or_ref); } + } else { + var content_iterator = content.iterator(); + if (content_iterator.next()) |entry| { + if (entry.value_ptr.schema) |schema_or_ref| { + schema = try self.convertSchemaOrReference(schema_or_ref); + } + } } } diff --git a/src/generators/converters/openapi32_converter.zig b/src/generators/converters/openapi32_converter.zig index 22084af..80cca2b 100644 --- a/src/generators/converters/openapi32_converter.zig +++ b/src/generators/converters/openapi32_converter.zig @@ -339,7 +339,11 @@ pub const OpenApi32Converter = struct { fn convertRequestBody(self: *OpenApi32Converter, requestBody: *const RequestBody32) !Parameter { var mut_request_body = requestBody.*; var schema: ?Schema = null; - if (mut_request_body.content.count() > 0) { + if (mut_request_body.content.get("application/json")) |media_type| { + if (media_type.schema) |schema_or_ref| { + schema = try self.convertSchemaOrReference(schema_or_ref); + } + } else if (mut_request_body.content.count() > 0) { var it = mut_request_body.content.iterator(); if (it.next()) |entry| { if (entry.value_ptr.schema) |schema_or_ref| { @@ -424,11 +428,17 @@ pub const OpenApi32Converter = struct { const description = response.description; var schema: ?Schema = null; if (response.content) |content| { - var content_iterator = content.iterator(); - if (content_iterator.next()) |entry| { - if (entry.value_ptr.schema) |schema_or_ref| { + if (content.get("application/json")) |media_type| { + if (media_type.schema) |schema_or_ref| { schema = try self.convertSchemaOrReference(schema_or_ref); } + } else { + var content_iterator = content.iterator(); + if (content_iterator.next()) |entry| { + if (entry.value_ptr.schema) |schema_or_ref| { + schema = try self.convertSchemaOrReference(schema_or_ref); + } + } } } diff --git a/src/generators/converters/openapi_converter.zig b/src/generators/converters/openapi_converter.zig index b00b501..6e29c4b 100644 --- a/src/generators/converters/openapi_converter.zig +++ b/src/generators/converters/openapi_converter.zig @@ -325,7 +325,11 @@ pub const OpenApiConverter = struct { fn convertRequestBody(self: *OpenApiConverter, requestBody: *const RequestBody3) !Parameter { var mut_request_body = requestBody.*; var schema: ?Schema = null; - if (mut_request_body.content.count() > 0) { + if (mut_request_body.content.get("application/json")) |media_type| { + if (media_type.schema) |schema_or_ref| { + schema = try self.convertSchemaOrReference(schema_or_ref); + } + } else if (mut_request_body.content.count() > 0) { var it = mut_request_body.content.iterator(); if (it.next()) |entry| { if (entry.value_ptr.schema) |schema_or_ref| { @@ -410,11 +414,17 @@ pub const OpenApiConverter = struct { const description = response.description; var schema: ?Schema = null; if (response.content) |content| { - var content_iterator = content.iterator(); - if (content_iterator.next()) |entry| { - if (entry.value_ptr.schema) |schema_or_ref| { + if (content.get("application/json")) |media_type| { + if (media_type.schema) |schema_or_ref| { schema = try self.convertSchemaOrReference(schema_or_ref); } + } else { + var content_iterator = content.iterator(); + if (content_iterator.next()) |entry| { + if (entry.value_ptr.schema) |schema_or_ref| { + schema = try self.convertSchemaOrReference(schema_or_ref); + } + } } } diff --git a/src/generators/unified/api_generator.zig b/src/generators/unified/api_generator.zig index 0cd034f..80e6d8d 100644 --- a/src/generators/unified/api_generator.zig +++ b/src/generators/unified/api_generator.zig @@ -2,12 +2,108 @@ const std = @import("std"); const cli = @import("../../cli.zig"); const UnifiedDocument = @import("../../models/common/document.zig").UnifiedDocument; const Operation = @import("../../models/common/document.zig").Operation; -const Parameter = @import("../../models/common/document.zig").Parameter; -const ParameterLocation = @import("../../models/common/document.zig").ParameterLocation; -const Response = @import("../../models/common/document.zig").Response; const Schema = @import("../../models/common/document.zig").Schema; const SchemaType = @import("../../models/common/document.zig").SchemaType; +fn isIdentStart(c: u8) bool { + return std.ascii.isAlphabetic(c) or c == '_'; +} + +fn isIdentContinue(c: u8) bool { + return std.ascii.isAlphanumeric(c) or c == '_'; +} + +fn isReservedIdent(name: []const u8) bool { + const reserved = [_][]const u8{ + "addrspace", "align", "allowzero", "and", "anyerror", "anyframe", "anyopaque", "anytype", + "asm", "async", "await", "bool", "break", "callconv", "catch", "comptime", + "const", "continue", "defer", "else", "enum", "errdefer", "error", "export", + "extern", "false", "fn", "for", "if", "inline", "isize", "linksection", + "noalias", "noreturn", "nosuspend", "null", "opaque", "or", "orelse", "packed", + "pub", "resume", "return", "struct", "suspend", "switch", "test", "threadlocal", + "true", "try", "type", "undefined", "union", "unreachable", "usize", "usingnamespace", + "var", "void", "volatile", "while", + }; + for (reserved) |word| { + if (std.mem.eql(u8, name, word)) return true; + } + return false; +} + +fn isBareIdentifier(name: []const u8) bool { + if (name.len == 0 or !isIdentStart(name[0]) or isReservedIdent(name)) return false; + for (name[1..]) |c| { + if (!isIdentContinue(c)) return false; + } + return true; +} + +const OperationRef = struct { + path: []const u8, + method: []const u8, + operation: Operation, +}; + +const ResourceWrapper = struct { + segments: [][]const u8, + method_name: []const u8, + operation_id: []const u8, + method: []const u8, + path: []const u8, + operation: Operation, + collides: bool = false, +}; + +fn operationRefLessThan(_: void, lhs: OperationRef, rhs: OperationRef) bool { + const path_order = std.mem.order(u8, lhs.path, rhs.path); + if (path_order != .eq) return path_order == .lt; + return std.mem.order(u8, lhs.method, rhs.method) == .lt; +} + +fn resourceWrapperLessThan(_: void, lhs: ResourceWrapper, rhs: ResourceWrapper) bool { + const segment_order = stringListOrder(lhs.segments, rhs.segments); + if (segment_order != .eq) return segment_order == .lt; + const method_order = std.mem.order(u8, lhs.method_name, rhs.method_name); + if (method_order != .eq) return method_order == .lt; + return std.mem.order(u8, lhs.operation_id, rhs.operation_id) == .lt; +} + +fn stringLessThan(_: void, lhs: []const u8, rhs: []const u8) bool { + return std.mem.order(u8, lhs, rhs) == .lt; +} + +fn stringListOrder(lhs: []const []const u8, rhs: []const []const u8) std.math.Order { + const len = @min(lhs.len, rhs.len); + for (lhs[0..len], rhs[0..len]) |lhs_item, rhs_item| { + const order = std.mem.order(u8, lhs_item, rhs_item); + if (order != .eq) return order; + } + return std.math.order(lhs.len, rhs.len); +} + +fn sameStringList(lhs: []const []const u8, rhs: []const []const u8) bool { + return stringListOrder(lhs, rhs) == .eq; +} + +fn containsString(values: []const []const u8, value: []const u8) bool { + for (values) |item| { + if (std.mem.eql(u8, item, value)) return true; + } + return false; +} + +fn isVersionSegment(segment: []const u8) bool { + if (segment.len < 2 or segment[0] != 'v') return false; + for (segment[1..]) |c| { + if (!std.ascii.isDigit(c)) return false; + } + return true; +} + +fn isPathParam(segment: []const u8) bool { + return segment.len >= 2 and segment[0] == '{' and segment[segment.len - 1] == '}'; +} + pub const UnifiedApiGenerator = struct { allocator: std.mem.Allocator, buffer: std.ArrayList(u8), @@ -29,13 +125,410 @@ pub const UnifiedApiGenerator = struct { self.buffer.clearRetainingCapacity(); try self.generateHeader(); try self.generateApiClient(document); + if (self.args.resource_wrappers != .none) { + try self.generateResourceWrappers(document); + } return try self.allocator.dupe(u8, self.buffer.items); } + fn appendIdentifier(self: *UnifiedApiGenerator, name: []const u8) !void { + if (isBareIdentifier(name)) { + try self.buffer.appendSlice(self.allocator, name); + return; + } + + try self.buffer.appendSlice(self.allocator, "@\""); + for (name) |c| { + switch (c) { + '\\', '"' => { + try self.buffer.append(self.allocator, '\\'); + try self.buffer.append(self.allocator, c); + }, + '\n' => try self.buffer.appendSlice(self.allocator, "\\n"), + '\r' => try self.buffer.appendSlice(self.allocator, "\\r"), + '\t' => try self.buffer.appendSlice(self.allocator, "\\t"), + else => try self.buffer.append(self.allocator, c), + } + } + try self.buffer.appendSlice(self.allocator, "\""); + } + + fn appendLineComment(self: *UnifiedApiGenerator, text: []const u8) !void { + var lines = std.mem.splitScalar(u8, text, '\n'); + while (lines.next()) |line| { + try self.buffer.appendSlice(self.allocator, "// "); + try self.buffer.appendSlice(self.allocator, std.mem.trim(u8, line, "\r")); + try self.buffer.appendSlice(self.allocator, "\n"); + } + } + fn generateHeader(self: *UnifiedApiGenerator) !void { try self.buffer.appendSlice(self.allocator, "///////////////////////////////////////////\n"); try self.buffer.appendSlice(self.allocator, "// Generated Zig API client from OpenAPI\n"); try self.buffer.appendSlice(self.allocator, "///////////////////////////////////////////\n\n"); + try self.buffer.appendSlice(self.allocator, + \\ + \\pub fn Owned(comptime T: type) type { + \\ return struct { + \\ allocator: std.mem.Allocator, + \\ body: []u8, + \\ parsed: std.json.Parsed(T), + \\ + \\ pub fn deinit(self: *@This()) void { + \\ self.parsed.deinit(); + \\ self.allocator.free(self.body); + \\ } + \\ + \\ pub fn value(self: *@This()) *T { + \\ return &self.parsed.value; + \\ } + \\ }; + \\} + \\ + \\pub const RawResponse = struct { + \\ allocator: std.mem.Allocator, + \\ status: std.http.Status, + \\ body: []u8, + \\ + \\ pub fn deinit(self: *@This()) void { + \\ self.allocator.free(self.body); + \\ } + \\}; + \\ + \\pub const ParseErrorResponse = struct { + \\ raw: RawResponse, + \\ error_name: []const u8, + \\}; + \\ + \\pub fn ApiResult(comptime T: type) type { + \\ return union(enum) { + \\ ok: Owned(T), + \\ api_error: RawResponse, + \\ parse_error: ParseErrorResponse, + \\ + \\ pub fn deinit(self: *@This()) void { + \\ switch (self.*) { + \\ .ok => |*value| value.deinit(), + \\ .api_error => |*value| value.deinit(), + \\ .parse_error => |*value| value.raw.deinit(), + \\ } + \\ } + \\ }; + \\} + \\ + \\pub const Client = struct { + \\ allocator: std.mem.Allocator, + \\ io: std.Io, + \\ http: std.http.Client, + \\ api_key: []const u8, + \\ base_url: []const u8 = " + ); + if (self.args.base_url) |base_url| try self.buffer.appendSlice(self.allocator, base_url); + try self.buffer.appendSlice(self.allocator, + \\", + \\ organization: ?[]const u8 = null, + \\ project: ?[]const u8 = null, + \\ default_headers: []const std.http.Header = &.{}, + \\ + \\ pub fn init(allocator: std.mem.Allocator, io: std.Io, api_key: []const u8) Client { + \\ return .{ + \\ .allocator = allocator, + \\ .io = io, + \\ .http = .{ .allocator = allocator, .io = io }, + \\ .api_key = api_key, + \\ }; + \\ } + \\ + \\ pub fn deinit(self: *Client) void { + \\ self.http.deinit(); + \\ } + \\ + \\ pub fn withBaseUrl(self: *Client, base_url: []const u8) void { + \\ self.base_url = base_url; + \\ } + \\}; + \\ + \\fn isQueryChar(c: u8) bool { + \\ return std.ascii.isAlphanumeric(c) or switch (c) { + \\ '-', '.', '_', '~' => true, + \\ else => false, + \\ }; + \\} + \\ + \\fn writeQueryComponent(writer: *std.Io.Writer, value: []const u8) !void { + \\ try std.Uri.Component.percentEncode(writer, value, isQueryChar); + \\} + \\ + \\fn writeQueryValue(writer: *std.Io.Writer, value: anytype) !void { + \\ const T = @TypeOf(value); + \\ switch (@typeInfo(T)) { + \\ .pointer => |ptr| { + \\ if (ptr.size == .slice and ptr.child == u8) { + \\ try writeQueryComponent(writer, value); + \\ } else { + \\ try std.json.Stringify.value(value, .{}, writer); + \\ } + \\ }, + \\ .int, .comptime_int, .float, .comptime_float, .bool => try writer.print("{}", .{value}), + \\ .@"enum" => try writeQueryComponent(writer, @tagName(value)), + \\ else => try std.json.Stringify.value(value, .{}, writer), + \\ } + \\} + \\ + \\fn appendQueryParam(writer: *std.Io.Writer, first_query: *bool, name: []const u8, value: anytype) !void { + \\ if (first_query.*) { + \\ try writer.writeByte('?'); + \\ first_query.* = false; + \\ } else { + \\ try writer.writeByte('&'); + \\ } + \\ try writeQueryComponent(writer, name); + \\ try writer.writeByte('='); + \\ try writeQueryValue(writer, value); + \\} + \\ + \\pub fn requestRaw(client: *Client, method: std.http.Method, url: []const u8, payload: ?[]const u8) !RawResponse { + \\ const allocator = client.allocator; + \\ var headers = std.ArrayList(std.http.Header).empty; + \\ defer headers.deinit(allocator); + \\ const auth_header = try appendClientHeaders(allocator, &headers, client, payload != null, "application/json"); + \\ defer if (auth_header) |value| allocator.free(value); + \\ + \\ const uri = try std.Uri.parse(url); + \\ var response_body: std.Io.Writer.Allocating = .init(allocator); + \\ defer response_body.deinit(); + \\ + \\ const result = try client.http.fetch(.{ + \\ .location = .{ .uri = uri }, + \\ .method = method, + \\ .extra_headers = headers.items, + \\ .payload = payload, + \\ .response_writer = &response_body.writer, + \\ }); + \\ + \\ return .{ + \\ .allocator = allocator, + \\ .status = result.status, + \\ .body = try response_body.toOwnedSlice(), + \\ }; + \\} + \\ + \\pub fn getRaw(client: *Client, path: []const u8) !RawResponse { + \\ const url = try std.fmt.allocPrint(client.allocator, "{s}{s}", .{ client.base_url, path }); + \\ defer client.allocator.free(url); + \\ return requestRaw(client, .GET, url, null); + \\} + \\ + \\pub fn postJsonRaw(client: *Client, path: []const u8, payload: anytype) !RawResponse { + \\ const allocator = client.allocator; + \\ var str: std.Io.Writer.Allocating = .init(allocator); + \\ defer str.deinit(); + \\ try std.json.Stringify.value(payload, .{ .emit_null_optional_fields = false }, &str.writer); + \\ + \\ const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ client.base_url, path }); + \\ defer allocator.free(url); + \\ return requestRaw(client, .POST, url, str.written()); + \\} + \\ + \\pub fn parseRawResponse(comptime T: type, raw: RawResponse) !ApiResult(T) { + \\ if (raw.status.class() != .success) return .{ .api_error = raw }; + \\ const parsed = std.json.parseFromSlice(T, raw.allocator, raw.body, .{ .ignore_unknown_fields = true }) catch |err| { + \\ return .{ .parse_error = .{ .raw = raw, .error_name = @errorName(err) } }; + \\ }; + \\ return .{ .ok = .{ .allocator = raw.allocator, .body = raw.body, .parsed = parsed } }; + \\} + \\ + \\pub fn getJsonResult(comptime T: type, client: *Client, path: []const u8) !ApiResult(T) { + \\ return parseRawResponse(T, try getRaw(client, path)); + \\} + \\ + \\pub fn postJsonResult(comptime T: type, client: *Client, path: []const u8, payload: anytype) !ApiResult(T) { + \\ return parseRawResponse(T, try postJsonRaw(client, path, payload)); + \\} + \\ + \\const max_sse_line_size = 256 * 1024; + \\const max_sse_event_size = 1024 * 1024; + \\ + \\pub fn parseSseBytes(allocator: std.mem.Allocator, bytes: []const u8, callback: anytype) !void { + \\ var reader: std.Io.Reader = .fixed(bytes); + \\ try parseSseReader(allocator, &reader, callback); + \\} + \\ + \\pub fn parseSseReader(allocator: std.mem.Allocator, reader: *std.Io.Reader, callback: anytype) !void { + \\ var line_buf: std.Io.Writer.Allocating = .init(allocator); + \\ defer line_buf.deinit(); + \\ + \\ var event_data: std.Io.Writer.Allocating = .init(allocator); + \\ defer event_data.deinit(); + \\ + \\ while (true) { + \\ line_buf.clearRetainingCapacity(); + \\ + \\ _ = reader.streamDelimiterLimit(&line_buf.writer, '\n', .limited(max_sse_line_size)) catch |err| switch (err) { + \\ error.StreamTooLong => return error.SseLineTooLong, + \\ error.ReadFailed => return err, + \\ error.WriteFailed => return err, + \\ }; + \\ + \\ const ended_with_delimiter = blk: { + \\ const byte = reader.peekByte() catch |err| switch (err) { + \\ error.EndOfStream => break :blk false, + \\ error.ReadFailed => return err, + \\ }; + \\ if (byte == '\n') { + \\ _ = try reader.takeByte(); + \\ break :blk true; + \\ } + \\ break :blk false; + \\ }; + \\ + \\ if (try processSseLine(&event_data, line_buf.written(), callback)) return; + \\ if (!ended_with_delimiter) break; + \\ } + \\ + \\ _ = try dispatchSseEvent(&event_data, callback); + \\} + \\ + \\fn processSseLine(event_data: *std.Io.Writer.Allocating, raw_line: []const u8, callback: anytype) !bool { + \\ const line = std.mem.trimEnd(u8, raw_line, "\r"); + \\ if (line.len == 0) return try dispatchSseEvent(event_data, callback); + \\ if (line[0] == ':') return false; + \\ + \\ const colon = std.mem.indexOfScalar(u8, line, ':') orelse return false; + \\ const field = line[0..colon]; + \\ if (!std.mem.eql(u8, field, "data")) return false; + \\ + \\ var value = line[colon + 1 ..]; + \\ if (value.len > 0 and value[0] == ' ') value = value[1..]; + \\ const separator_len: usize = if (event_data.written().len == 0) 0 else 1; + \\ if (event_data.written().len + separator_len + value.len > max_sse_event_size) return error.SseEventTooLong; + \\ if (separator_len != 0) try event_data.writer.writeByte('\n'); + \\ try event_data.writer.writeAll(value); + \\ return false; + \\} + \\ + \\fn dispatchSseEvent(event_data: *std.Io.Writer.Allocating, callback: anytype) !bool { + \\ const data = event_data.written(); + \\ if (data.len == 0) return false; + \\ defer event_data.clearRetainingCapacity(); + \\ + \\ if (std.mem.eql(u8, data, "[DONE]")) return true; + \\ try callback.event(data); + \\ return false; + \\} + \\ + \\fn TypedSseCallback(comptime T: type, comptime Callback: type) type { + \\ return struct { + \\ allocator: std.mem.Allocator, + \\ callback: *Callback, + \\ + \\ pub fn event(self: *@This(), data: []const u8) !void { + \\ var parsed = try std.json.parseFromSlice(T, self.allocator, data, .{ .ignore_unknown_fields = true }); + \\ defer parsed.deinit(); + \\ try self.callback.event(&parsed.value); + \\ } + \\ }; + \\} + \\ + \\pub fn parseSseBytesTyped(comptime T: type, allocator: std.mem.Allocator, bytes: []const u8, callback: anytype) !void { + \\ const Callback = @TypeOf(callback.*); + \\ var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = allocator, .callback = callback }; + \\ try parseSseBytes(allocator, bytes, &typed_callback); + \\} + \\ + \\pub fn parseSseReaderTyped(comptime T: type, allocator: std.mem.Allocator, reader: *std.Io.Reader, callback: anytype) !void { + \\ const Callback = @TypeOf(callback.*); + \\ var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = allocator, .callback = callback }; + \\ try parseSseReader(allocator, reader, &typed_callback); + \\} + \\ + \\fn stringifyStreamRequest(allocator: std.mem.Allocator, requestBody: anytype) ![]u8 { + \\ var str: std.Io.Writer.Allocating = .init(allocator); + \\ defer str.deinit(); + \\ try std.json.Stringify.value(requestBody, .{ .emit_null_optional_fields = false }, &str.writer); + \\ + \\ var parsed = try std.json.parseFromSlice(std.json.Value, allocator, str.written(), .{ .ignore_unknown_fields = true }); + \\ defer parsed.deinit(); + \\ + \\ if (parsed.value == .object) { + \\ try parsed.value.object.put(parsed.arena.allocator(), "stream", .{ .bool = true }); + \\ } + \\ + \\ var out: std.Io.Writer.Allocating = .init(allocator); + \\ errdefer out.deinit(); + \\ try std.json.Stringify.value(parsed.value, .{ .emit_null_optional_fields = false }, &out.writer); + \\ return try out.toOwnedSlice(); + \\} + \\ + \\fn streamJsonTyped(comptime T: type, client: *Client, path: []const u8, requestBody: anytype, callback: anytype) !void { + \\ const Callback = @TypeOf(callback.*); + \\ var typed_callback: TypedSseCallback(T, Callback) = .{ .allocator = client.allocator, .callback = callback }; + \\ try streamJson(client, path, requestBody, &typed_callback); + \\} + \\ + \\fn streamJson(client: *Client, path: []const u8, requestBody: anytype, callback: anytype) !void { + \\ const allocator = client.allocator; + \\ const payload = try stringifyStreamRequest(allocator, requestBody); + \\ defer allocator.free(payload); + \\ + \\ var headers = std.ArrayList(std.http.Header).empty; + \\ defer headers.deinit(allocator); + \\ const auth_header = try appendClientHeaders(allocator, &headers, client, true, "text/event-stream"); + \\ defer if (auth_header) |value| allocator.free(value); + \\ + \\ const url = try std.fmt.allocPrint(allocator, "{s}{s}", .{ client.base_url, path }); + \\ defer allocator.free(url); + \\ const uri = try std.Uri.parse(url); + \\ + \\ var req = try client.http.request(.POST, uri, .{ + \\ .redirect_behavior = .unhandled, + \\ .headers = .{ .accept_encoding = .{ .override = "identity" } }, + \\ .extra_headers = headers.items, + \\ }); + \\ defer req.deinit(); + \\ + \\ req.transfer_encoding = .{ .content_length = payload.len }; + \\ var body = try req.sendBodyUnflushed(&.{}); + \\ try body.writer.writeAll(payload); + \\ try body.end(); + \\ try req.connection.?.flush(); + \\ + \\ var response = try req.receiveHead(&.{}); + \\ if (response.head.status.class() != .success) return error.ResponseError; + \\ + \\ var transfer_buffer: [8 * 1024]u8 = undefined; + \\ const reader = response.reader(&transfer_buffer); + \\ parseSseReader(allocator, reader, callback) catch |err| switch (err) { + \\ error.ReadFailed => return response.bodyErr() orelse err, + \\ else => return err, + \\ }; + \\} + \\ + \\fn appendClientHeaders(allocator: std.mem.Allocator, headers: *std.ArrayList(std.http.Header), client: *Client, include_content_type: bool, accept: []const u8) !?[]u8 { + \\ if (include_content_type) { + \\ try headers.append(allocator, .{ .name = "Content-Type", .value = "application/json" }); + \\ } + \\ try headers.append(allocator, .{ .name = "Accept", .value = accept }); + \\ + \\ var auth_header: ?[]u8 = null; + \\ if (client.api_key.len > 0) { + \\ auth_header = try std.fmt.allocPrint(allocator, "Bearer {s}", .{client.api_key}); + \\ try headers.append(allocator, .{ .name = "Authorization", .value = auth_header.? }); + \\ } + \\ if (client.organization) |organization| { + \\ try headers.append(allocator, .{ .name = "OpenAI-Organization", .value = organization }); + \\ } + \\ if (client.project) |project| { + \\ try headers.append(allocator, .{ .name = "OpenAI-Project", .value = project }); + \\ } + \\ for (client.default_headers) |header| { + \\ try headers.append(allocator, header); + \\ } + \\ return auth_header; + \\} + \\ + \\ + ); } fn generateApiClient(self: *UnifiedApiGenerator, document: UnifiedDocument) !void { @@ -61,23 +554,689 @@ pub const UnifiedApiGenerator = struct { try self.generateComments(operation); try self.generateFunctionSignature(method, path, operation); try self.generateFunctionBody(method, path, operation); + if (operation.operationId != null) { + try self.generateFunctionRaw(method, path, operation); + } + if (operation.operationId != null and self.hasReturnValue(method, operation)) { + try self.generateFunctionResult(method, path, operation); + } + + if (operation.operationId) |op_id| { + if (std.mem.eql(u8, method, "POST") and std.mem.eql(u8, op_id, "createChatCompletion")) { + try self.generateStreamFunction("streamChatCompletion", "CreateChatCompletionRequest", path); + } else if (std.mem.eql(u8, method, "POST") and std.mem.eql(u8, op_id, "createResponse")) { + try self.generateStreamFunction("streamResponse", "CreateResponse", path); + } + } + } + + fn generateFunctionResult(self: *UnifiedApiGenerator, method: []const u8, path: []const u8, operation: Operation) !void { + _ = path; + const operation_id = operation.operationId orelse return; + const result_name = try std.fmt.allocPrint(self.allocator, "{s}Result", .{operation_id}); + defer self.allocator.free(result_name); + const raw_name = try std.fmt.allocPrint(self.allocator, "{s}Raw", .{operation_id}); + defer self.allocator.free(raw_name); + + try self.buffer.appendSlice(self.allocator, "pub fn "); + try self.appendIdentifier(result_name); + try self.buffer.appendSlice(self.allocator, "(client: *Client"); + try self.appendFlatOperationParameters(operation); + try self.buffer.appendSlice(self.allocator, ") !ApiResult("); + try self.appendReturnType(method, operation); + try self.buffer.appendSlice(self.allocator, ") {\n"); + try self.buffer.appendSlice(self.allocator, " return parseRawResponse("); + try self.appendReturnType(method, operation); + try self.buffer.appendSlice(self.allocator, ", try "); + try self.appendIdentifier(raw_name); + try self.appendFlatCallArguments(operation); + try self.buffer.appendSlice(self.allocator, ");\n"); + try self.buffer.appendSlice(self.allocator, "}\n\n"); + } + + fn generateFunctionRaw(self: *UnifiedApiGenerator, method: []const u8, path: []const u8, operation: Operation) !void { + const operation_id = operation.operationId orelse return; + const raw_name = try std.fmt.allocPrint(self.allocator, "{s}Raw", .{operation_id}); + defer self.allocator.free(raw_name); + + try self.buffer.appendSlice(self.allocator, "pub fn "); + try self.appendIdentifier(raw_name); + try self.buffer.appendSlice(self.allocator, "(client: *Client"); + try self.appendFlatOperationParameters(operation); + try self.buffer.appendSlice(self.allocator, ") !RawResponse {\n"); + try self.buffer.appendSlice(self.allocator, " const allocator = client.allocator;\n"); + try self.appendUnusedParameters(operation); + try self.appendUrlConstruction(path, operation); + + if (self.hasBodyParameter(operation)) { + 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"); + 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: ?[]const u8 = str.written();\n"); + } else { + try self.buffer.appendSlice(self.allocator, " const payload: ?[]const u8 = null;\n"); + } + + try self.buffer.appendSlice(self.allocator, "\n return requestRaw(client, std.http.Method."); + try self.buffer.appendSlice(self.allocator, method); + try self.buffer.appendSlice(self.allocator, ", uri_buf.written(), payload);\n"); + try self.buffer.appendSlice(self.allocator, "}\n\n"); + } + + fn appendFlatCallArguments(self: *UnifiedApiGenerator, operation: Operation) !void { + try self.buffer.appendSlice(self.allocator, "(client"); + if (operation.parameters) |params| { + for (params) |param| { + try self.buffer.appendSlice(self.allocator, ", "); + const name: []const u8 = if (param.location == .body) "requestBody" else param.name; + try self.appendIdentifier(name); + } + } + try self.buffer.appendSlice(self.allocator, ")"); + } + + fn appendFlatOperationParameters(self: *UnifiedApiGenerator, operation: Operation) !void { + if (operation.parameters) |params| { + for (params) |param| { + try self.buffer.appendSlice(self.allocator, ", "); + const name: []const u8 = if (param.location == .body) "requestBody" else param.name; + try self.appendIdentifier(name); + try self.buffer.appendSlice(self.allocator, ": "); + if (param.location == .body) { + if (param.schema) |schema| { + try self.appendZigTypeFromSchema(schema); + } else { + 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.schema) |schema| { + try self.appendZigQueryTypeFromSchema(schema); + } else if (param.type) |param_type| { + try self.appendZigTypeFromSchemaType(param_type); + } else { + try self.buffer.appendSlice(self.allocator, "[]const u8"); + } + } + } + } + } + + fn appendUnusedParameters(self: *UnifiedApiGenerator, operation: Operation) !void { + if (operation.parameters) |parameters| { + for (parameters) |parameter| { + if (parameter.location != .path and parameter.location != .body and parameter.location != .query) { + try self.buffer.appendSlice(self.allocator, " _ = "); + try self.appendIdentifier(parameter.name); + try self.buffer.appendSlice(self.allocator, ";\n"); + } + } + } + } + + fn appendUrlConstruction(self: *UnifiedApiGenerator, path: []const u8, operation: Operation) !void { + var new_path = path; + var allocated_paths = std.ArrayList([]u8).empty; + defer { + for (allocated_paths.items) |allocated_path| self.allocator.free(allocated_path); + allocated_paths.deinit(self.allocator); + } + + if (operation.parameters) |parameters| { + for (parameters) |parameter| { + if (parameter.location != .path) continue; + const param = parameter.name; + const path_type = if (parameter.schema) |schema| + schema.type orelse .string + else + parameter.type orelse .string; + const param_type = switch (path_type) { + .string => "s", + .integer => "d", + .number => "d", + else => "any", + }; + const size = std.mem.replacementSize(u8, new_path, param, param_type); + const output = try self.allocator.alloc(u8, size); + try allocated_paths.append(self.allocator, output); + _ = std.mem.replace(u8, new_path, param, param_type, output); + new_path = output; + } + } + + try self.buffer.appendSlice(self.allocator, " var uri_buf: std.Io.Writer.Allocating = .init(allocator);\n"); + try self.buffer.appendSlice(self.allocator, " defer uri_buf.deinit();\n"); + try self.buffer.appendSlice(self.allocator, " try uri_buf.writer.print(\"{s}"); + try self.buffer.appendSlice(self.allocator, new_path); + try self.buffer.appendSlice(self.allocator, "\", .{client.base_url"); + if (operation.parameters) |parameters| { + for (parameters) |parameter| { + if (parameter.location != .path) continue; + try self.buffer.appendSlice(self.allocator, ", "); + try self.appendIdentifier(parameter.name); + } + } + try self.buffer.appendSlice(self.allocator, "});\n"); + + var has_query_param = false; + if (operation.parameters) |parameters| { + for (parameters) |parameter| { + if (parameter.location == .query) { + has_query_param = true; + break; + } + } + } + if (has_query_param) { + try self.buffer.appendSlice(self.allocator, " var first_query = true;\n"); + if (operation.parameters) |parameters| { + for (parameters) |parameter| { + if (parameter.location != .query) continue; + if (parameter.required) { + try self.buffer.appendSlice(self.allocator, " try appendQueryParam(&uri_buf.writer, &first_query, \""); + try self.buffer.appendSlice(self.allocator, parameter.name); + try self.buffer.appendSlice(self.allocator, "\", "); + try self.appendIdentifier(parameter.name); + try self.buffer.appendSlice(self.allocator, ");\n"); + } else { + try self.buffer.appendSlice(self.allocator, " if ("); + try self.appendIdentifier(parameter.name); + try self.buffer.appendSlice(self.allocator, ") |value| {\n"); + try self.buffer.appendSlice(self.allocator, " try appendQueryParam(&uri_buf.writer, &first_query, \""); + try self.buffer.appendSlice(self.allocator, parameter.name); + try self.buffer.appendSlice(self.allocator, "\", value);\n"); + try self.buffer.appendSlice(self.allocator, " }\n"); + } + } + } + } + } + + fn hasBodyParameter(self: *UnifiedApiGenerator, operation: Operation) bool { + _ = self; + if (operation.parameters) |params| { + for (params) |param| { + if (param.location == .body) return true; + } + } + return false; + } + + fn generateStreamFunction(self: *UnifiedApiGenerator, name: []const u8, request_type: []const u8, path: []const u8) !void { + try self.buffer.appendSlice(self.allocator, "pub fn "); + try self.buffer.appendSlice(self.allocator, name); + try self.buffer.appendSlice(self.allocator, "(client: *Client, requestBody: "); + try self.buffer.appendSlice(self.allocator, request_type); + try self.buffer.appendSlice(self.allocator, ", callback: anytype) !void {\n"); + try self.buffer.appendSlice(self.allocator, " return streamJson(client, \""); + try self.buffer.appendSlice(self.allocator, path); + try self.buffer.appendSlice(self.allocator, "\", requestBody, callback);\n"); + try self.buffer.appendSlice(self.allocator, "}\n\n"); + + try self.buffer.appendSlice(self.allocator, "pub fn "); + try self.buffer.appendSlice(self.allocator, name); + try self.buffer.appendSlice(self.allocator, "Events(comptime Event: type, client: *Client, requestBody: "); + try self.buffer.appendSlice(self.allocator, request_type); + try self.buffer.appendSlice(self.allocator, ", callback: anytype) !void {\n"); + try self.buffer.appendSlice(self.allocator, " return streamJsonTyped(Event, client, \""); + try self.buffer.appendSlice(self.allocator, path); + try self.buffer.appendSlice(self.allocator, "\", requestBody, callback);\n"); + try self.buffer.appendSlice(self.allocator, "}\n\n"); + } + + fn generateResourceWrappers(self: *UnifiedApiGenerator, document: UnifiedDocument) !void { + var operations = std.ArrayList(OperationRef).empty; + defer operations.deinit(self.allocator); + + var path_iterator = document.paths.iterator(); + while (path_iterator.next()) |entry| { + const path = entry.key_ptr.*; + const path_item = entry.value_ptr.*; + if (path_item.get) |op| try operations.append(self.allocator, .{ .path = path, .method = "GET", .operation = op }); + if (path_item.post) |op| try operations.append(self.allocator, .{ .path = path, .method = "POST", .operation = op }); + if (path_item.put) |op| try operations.append(self.allocator, .{ .path = path, .method = "PUT", .operation = op }); + if (path_item.delete) |op| try operations.append(self.allocator, .{ .path = path, .method = "DELETE", .operation = op }); + if (path_item.patch) |op| try operations.append(self.allocator, .{ .path = path, .method = "PATCH", .operation = op }); + if (path_item.head) |op| try operations.append(self.allocator, .{ .path = path, .method = "HEAD", .operation = op }); + if (path_item.options) |op| try operations.append(self.allocator, .{ .path = path, .method = "OPTIONS", .operation = op }); + } + std.mem.sort(OperationRef, operations.items, {}, operationRefLessThan); + + var wrappers = std.ArrayList(ResourceWrapper).empty; + defer { + for (wrappers.items) |wrapper| { + for (wrapper.segments) |segment| self.allocator.free(segment); + self.allocator.free(wrapper.segments); + self.allocator.free(wrapper.method_name); + } + wrappers.deinit(self.allocator); + } + + for (operations.items) |op_ref| { + const operation_id = op_ref.operation.operationId orelse continue; + const segments = try self.resourceSegments(op_ref.path, op_ref.operation); + if (segments.len == 0) { + self.allocator.free(segments); + continue; + } + errdefer { + for (segments) |segment| self.allocator.free(segment); + self.allocator.free(segments); + } + + try wrappers.append(self.allocator, .{ + .segments = segments, + .method_name = try self.resourceMethodName(operation_id, op_ref.method), + .operation_id = operation_id, + .method = op_ref.method, + .path = op_ref.path, + .operation = op_ref.operation, + }); + } + + for (wrappers.items, 0..) |*left, i| { + for (wrappers.items[i + 1 ..]) |*right| { + if (sameStringList(left.segments, right.segments) and std.mem.eql(u8, left.method_name, right.method_name)) { + left.collides = true; + right.collides = true; + } + } + } + + std.mem.sort(ResourceWrapper, wrappers.items, {}, resourceWrapperLessThan); + + try self.buffer.appendSlice(self.allocator, "pub const resources = struct {\n"); + try self.generateResourceLevel(wrappers.items, 0, 1, &.{}); + try self.buffer.appendSlice(self.allocator, "};\n\n"); + + var top_segments = std.ArrayList([]const u8).empty; + defer top_segments.deinit(self.allocator); + for (wrappers.items) |wrapper| { + const top = wrapper.segments[0]; + if (!containsString(top_segments.items, top)) try top_segments.append(self.allocator, top); + } + std.mem.sort([]const u8, top_segments.items, {}, stringLessThan); + for (top_segments.items) |top| { + if (self.resourceAliasConflicts(top, document)) continue; + try self.buffer.appendSlice(self.allocator, "pub const "); + try self.buffer.appendSlice(self.allocator, top); + try self.buffer.appendSlice(self.allocator, " = resources."); + try self.buffer.appendSlice(self.allocator, top); + try self.buffer.appendSlice(self.allocator, ";\n"); + } + if (top_segments.items.len > 0) try self.buffer.appendSlice(self.allocator, "\n"); + } + + fn generateResourceLevel(self: *UnifiedApiGenerator, wrappers: []ResourceWrapper, depth: usize, indent: usize, ancestor_names: []const []const u8) !void { + var children = std.ArrayList([]const u8).empty; + defer children.deinit(self.allocator); + for (wrappers) |wrapper| { + if (wrapper.segments.len > depth and !containsString(children.items, wrapper.segments[depth])) { + try children.append(self.allocator, wrapper.segments[depth]); + } + } + std.mem.sort([]const u8, children.items, {}, stringLessThan); + + var declarations = std.ArrayList([]const u8).empty; + defer declarations.deinit(self.allocator); + try declarations.appendSlice(self.allocator, ancestor_names); + var allocated_declarations = std.ArrayList([]const u8).empty; + defer { + for (allocated_declarations.items) |name| self.allocator.free(name); + allocated_declarations.deinit(self.allocator); + } + + for (children.items) |child| try declarations.append(self.allocator, child); + for (wrappers) |wrapper| { + if (wrapper.segments.len == depth) { + const name = try self.resourceWrapperNameAlloc(wrapper); + try allocated_declarations.append(self.allocator, name); + try declarations.append(self.allocator, name); + if (self.hasReturnValue(wrapper.method, wrapper.operation)) { + const result_name = try std.fmt.allocPrint(self.allocator, "{s}Result", .{name}); + try allocated_declarations.append(self.allocator, result_name); + try declarations.append(self.allocator, result_name); + } + if (self.streamFunctionName(wrapper.operation_id) != null) { + try declarations.append(self.allocator, "stream"); + try declarations.append(self.allocator, "streamEvents"); + } + } + } + + for (wrappers) |wrapper| { + if (wrapper.segments.len == depth) { + try self.generateResourceMethod(wrapper, declarations.items, indent); + if (self.hasReturnValue(wrapper.method, wrapper.operation)) { + try self.generateResourceResultMethod(wrapper, declarations.items, indent); + } + if (self.streamFunctionName(wrapper.operation_id)) |stream_name| { + try self.generateResourceStreamMethods(wrapper, stream_name, indent); + } + } + } + + for (children.items) |child| { + try self.appendIndent(indent); + try self.buffer.appendSlice(self.allocator, "pub const "); + try self.buffer.appendSlice(self.allocator, child); + try self.buffer.appendSlice(self.allocator, " = struct {\n"); + + var child_wrappers = std.ArrayList(ResourceWrapper).empty; + defer child_wrappers.deinit(self.allocator); + for (wrappers) |wrapper| { + if (wrapper.segments.len > depth and std.mem.eql(u8, wrapper.segments[depth], child)) { + try child_wrappers.append(self.allocator, wrapper); + } + } + try self.generateResourceLevel(child_wrappers.items, depth + 1, indent + 1, declarations.items); + + try self.appendIndent(indent); + try self.buffer.appendSlice(self.allocator, "};\n"); + } + } + + fn generateResourceMethod(self: *UnifiedApiGenerator, wrapper: ResourceWrapper, forbidden_names: []const []const u8, indent: usize) !void { + try self.appendIndent(indent); + try self.buffer.appendSlice(self.allocator, "pub fn "); + const wrapper_name = try self.resourceWrapperNameAlloc(wrapper); + defer self.allocator.free(wrapper_name); + try self.buffer.appendSlice(self.allocator, wrapper_name); + try self.appendWrapperSignatureAndReturn(wrapper.method, wrapper.operation, forbidden_names); + try self.buffer.appendSlice(self.allocator, " {\n"); + try self.appendIndent(indent + 1); + try self.buffer.appendSlice(self.allocator, "return "); + try self.appendIdentifier(wrapper.operation_id); + try self.appendWrapperCallArguments(wrapper.operation, forbidden_names); + try self.buffer.appendSlice(self.allocator, ";\n"); + try self.appendIndent(indent); + try self.buffer.appendSlice(self.allocator, "}\n"); + } + + fn generateResourceResultMethod(self: *UnifiedApiGenerator, wrapper: ResourceWrapper, forbidden_names: []const []const u8, indent: usize) !void { + const wrapper_name = try self.resourceWrapperNameAlloc(wrapper); + defer self.allocator.free(wrapper_name); + const result_name = try std.fmt.allocPrint(self.allocator, "{s}Result", .{wrapper_name}); + defer self.allocator.free(result_name); + const operation_result_name = try std.fmt.allocPrint(self.allocator, "{s}Result", .{wrapper.operation_id}); + defer self.allocator.free(operation_result_name); + + try self.appendIndent(indent); + try self.buffer.appendSlice(self.allocator, "pub fn "); + try self.buffer.appendSlice(self.allocator, result_name); + try self.appendWrapperResultSignature(wrapper.method, wrapper.operation, forbidden_names); + try self.buffer.appendSlice(self.allocator, " {\n"); + try self.appendIndent(indent + 1); + try self.buffer.appendSlice(self.allocator, "return "); + try self.appendIdentifier(operation_result_name); + try self.appendWrapperCallArguments(wrapper.operation, forbidden_names); + try self.buffer.appendSlice(self.allocator, ";\n"); + try self.appendIndent(indent); + try self.buffer.appendSlice(self.allocator, "}\n"); + } + + fn appendWrapperResultSignature(self: *UnifiedApiGenerator, method: []const u8, operation: Operation, forbidden_names: []const []const u8) !void { + try self.buffer.appendSlice(self.allocator, "(client: *Client"); + try self.appendOperationParameters(operation, forbidden_names); + try self.buffer.appendSlice(self.allocator, ") !ApiResult("); + try self.appendReturnType(method, operation); + try self.buffer.appendSlice(self.allocator, ")"); + } + + fn resourceWrapperNameAlloc(self: *UnifiedApiGenerator, wrapper: ResourceWrapper) ![]const u8 { + if (!wrapper.collides) return try self.allocator.dupe(u8, wrapper.method_name); + const collision_name = try self.sanitizeIdentifierAlloc(wrapper.operation_id); + defer self.allocator.free(collision_name); + return try std.fmt.allocPrint(self.allocator, "{s}_", .{collision_name}); + } + + fn generateResourceStreamMethods(self: *UnifiedApiGenerator, wrapper: ResourceWrapper, stream_name: []const u8, indent: usize) !void { + const request_type = self.bodyTypeName(wrapper.operation) orelse return; + + try self.appendIndent(indent); + try self.buffer.appendSlice(self.allocator, "pub fn stream(client: *Client, requestBody: "); + try self.buffer.appendSlice(self.allocator, request_type); + try self.buffer.appendSlice(self.allocator, ", callback: anytype) !void {\n"); + try self.appendIndent(indent + 1); + try self.buffer.appendSlice(self.allocator, "return "); + try self.buffer.appendSlice(self.allocator, stream_name); + try self.buffer.appendSlice(self.allocator, "(client, requestBody, callback);\n"); + try self.appendIndent(indent); + try self.buffer.appendSlice(self.allocator, "}\n"); + + try self.appendIndent(indent); + try self.buffer.appendSlice(self.allocator, "pub fn streamEvents(comptime Event: type, client: *Client, requestBody: "); + try self.buffer.appendSlice(self.allocator, request_type); + try self.buffer.appendSlice(self.allocator, ", callback: anytype) !void {\n"); + try self.appendIndent(indent + 1); + try self.buffer.appendSlice(self.allocator, "return "); + try self.buffer.appendSlice(self.allocator, stream_name); + try self.buffer.appendSlice(self.allocator, "Events(Event, client, requestBody, callback);\n"); + try self.appendIndent(indent); + try self.buffer.appendSlice(self.allocator, "}\n"); + } + + fn appendWrapperSignatureAndReturn(self: *UnifiedApiGenerator, method: []const u8, operation: Operation, forbidden_names: []const []const u8) !void { + try self.buffer.appendSlice(self.allocator, "(client: *Client"); + try self.appendOperationParameters(operation, forbidden_names); + if (self.hasReturnValue(method, operation)) { + try self.buffer.appendSlice(self.allocator, ") !Owned("); + try self.appendReturnType(method, operation); + try self.buffer.appendSlice(self.allocator, ")"); + } else { + try self.buffer.appendSlice(self.allocator, ") !void"); + } + } + + fn appendOperationParameters(self: *UnifiedApiGenerator, operation: Operation, forbidden_names: []const []const u8) !void { + if (operation.parameters) |params| { + for (params) |param| { + try self.buffer.appendSlice(self.allocator, ", "); + const name: []const u8 = if (param.location == .body) "requestBody" else param.name; + try self.appendParameterName(name, forbidden_names); + try self.buffer.appendSlice(self.allocator, ": "); + if (param.location == .body) { + if (param.schema) |schema| { + try self.appendZigTypeFromSchema(schema); + } else { + 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.schema) |schema| { + try self.appendZigQueryTypeFromSchema(schema); + } else if (param.type) |param_type| { + try self.appendZigTypeFromSchemaType(param_type); + } else { + try self.buffer.appendSlice(self.allocator, "[]const u8"); + } + } + } + } + } + + fn appendWrapperCallArguments(self: *UnifiedApiGenerator, operation: Operation, forbidden_names: []const []const u8) !void { + try self.buffer.appendSlice(self.allocator, "(client"); + if (operation.parameters) |params| { + for (params) |param| { + try self.buffer.appendSlice(self.allocator, ", "); + const name: []const u8 = if (param.location == .body) "requestBody" else param.name; + try self.appendParameterName(name, forbidden_names); + } + } + try self.buffer.appendSlice(self.allocator, ")"); + } + + fn appendParameterName(self: *UnifiedApiGenerator, name: []const u8, forbidden_names: []const []const u8) !void { + if (containsString(forbidden_names, name)) { + const safe_name = try self.sanitizeIdentifierAlloc(name); + defer self.allocator.free(safe_name); + try self.buffer.appendSlice(self.allocator, safe_name); + try self.buffer.appendSlice(self.allocator, "_param"); + } else { + try self.appendIdentifier(name); + } + } + + fn bodyTypeName(self: *UnifiedApiGenerator, operation: Operation) ?[]const u8 { + if (operation.parameters) |params| { + for (params) |param| { + if (param.location == .body) { + if (param.schema) |schema| { + if (schema.ref) |ref| { + if (std.mem.lastIndexOf(u8, ref, "/")) |last_slash| return ref[last_slash + 1 ..]; + } + } + } + } + } + _ = self; + return null; + } + + fn streamFunctionName(self: *UnifiedApiGenerator, operation_id: []const u8) ?[]const u8 { + _ = self; + if (std.mem.eql(u8, operation_id, "createChatCompletion")) return "streamChatCompletion"; + if (std.mem.eql(u8, operation_id, "createResponse")) return "streamResponse"; + return null; + } + + fn resourceAliasConflicts(self: *UnifiedApiGenerator, alias: []const u8, document: UnifiedDocument) bool { + const reserved_aliases = [_][]const u8{ "organization", "project", "value" }; + for (reserved_aliases) |reserved_alias| { + if (std.mem.eql(u8, alias, reserved_alias)) return true; + } + + var path_iterator = document.paths.iterator(); + while (path_iterator.next()) |entry| { + const path_item = entry.value_ptr.*; + if (operationHasParameterNamed(self, path_item.get, alias)) return true; + if (operationHasParameterNamed(self, path_item.post, alias)) return true; + if (operationHasParameterNamed(self, path_item.put, alias)) return true; + if (operationHasParameterNamed(self, path_item.delete, alias)) return true; + if (operationHasParameterNamed(self, path_item.patch, alias)) return true; + if (operationHasParameterNamed(self, path_item.head, alias)) return true; + if (operationHasParameterNamed(self, path_item.options, alias)) return true; + } + return false; + } + + fn operationHasParameterNamed(self: *UnifiedApiGenerator, maybe_operation: ?Operation, name: []const u8) bool { + const operation = maybe_operation orelse return false; + if (operation.parameters) |params| { + for (params) |param| { + const param_name: []const u8 = if (param.location == .body) "requestBody" else param.name; + const sanitized = self.sanitizeIdentifierAlloc(param_name) catch return true; + defer self.allocator.free(sanitized); + if (std.mem.eql(u8, sanitized, name)) return true; + } + } + return false; + } + + fn resourceSegments(self: *UnifiedApiGenerator, path: []const u8, operation: Operation) ![][]const u8 { + return switch (self.args.resource_wrappers) { + .none => self.allocator.alloc([]const u8, 0), + .paths => self.resourceSegmentsFromPath(path), + .tags => if (operation.tags) |tags| blk: { + if (tags.len > 0) { + const segments = try self.allocator.alloc([]const u8, 1); + segments[0] = try self.sanitizeIdentifierAlloc(tags[0]); + break :blk segments; + } + break :blk try self.resourceSegmentsFromPath(path); + } else self.resourceSegmentsFromPath(path), + .hybrid => self.resourceSegmentsHybrid(path, operation), + }; + } + + fn resourceSegmentsHybrid(self: *UnifiedApiGenerator, path: []const u8, operation: Operation) ![][]const u8 { + const path_segments = try self.resourceSegmentsFromPath(path); + errdefer { + for (path_segments) |segment| self.allocator.free(segment); + self.allocator.free(path_segments); + } + + if (operation.tags == null or operation.tags.?.len == 0) return path_segments; + const tag = try self.sanitizeIdentifierAlloc(operation.tags.?[0]); + errdefer self.allocator.free(tag); + if (path_segments.len > 0 and std.mem.eql(u8, path_segments[0], tag)) { + self.allocator.free(tag); + return path_segments; + } + + const segments = try self.allocator.alloc([]const u8, path_segments.len + 1); + segments[0] = tag; + for (path_segments, 0..) |segment, i| segments[i + 1] = segment; + self.allocator.free(path_segments); + return segments; + } + + fn resourceSegmentsFromPath(self: *UnifiedApiGenerator, path: []const u8) ![][]const u8 { + var segments = std.ArrayList([]const u8).empty; + errdefer { + for (segments.items) |segment| self.allocator.free(segment); + segments.deinit(self.allocator); + } + + var iterator = std.mem.splitScalar(u8, path, '/'); + while (iterator.next()) |raw_segment| { + if (raw_segment.len == 0 or isVersionSegment(raw_segment) or isPathParam(raw_segment)) continue; + try segments.append(self.allocator, try self.sanitizeIdentifierAlloc(raw_segment)); + } + return try segments.toOwnedSlice(self.allocator); + } + + fn resourceMethodName(self: *UnifiedApiGenerator, operation_id: []const u8, method: []const u8) ![]const u8 { + _ = method; + const verbs = [_]struct { prefix: []const u8, name: []const u8 }{ + .{ .prefix = "create", .name = "create" }, + .{ .prefix = "list", .name = "list" }, + .{ .prefix = "retrieve", .name = "retrieve" }, + .{ .prefix = "get", .name = "get" }, + .{ .prefix = "delete", .name = "delete" }, + .{ .prefix = "update", .name = "update" }, + .{ .prefix = "modify", .name = "update" }, + .{ .prefix = "cancel", .name = "cancel" }, + }; + for (verbs) |verb| { + if (std.mem.startsWith(u8, operation_id, verb.prefix) and + (operation_id.len == verb.prefix.len or std.ascii.isUpper(operation_id[verb.prefix.len]))) + { + return try self.allocator.dupe(u8, verb.name); + } + } + return self.sanitizeIdentifierAlloc(operation_id); + } + + fn sanitizeIdentifierAlloc(self: *UnifiedApiGenerator, value: []const u8) ![]const u8 { + var out = std.ArrayList(u8).empty; + errdefer out.deinit(self.allocator); + for (value, 0..) |c, i| { + const lower = std.ascii.toLower(c); + const valid = if (i == 0) isIdentStart(lower) else isIdentContinue(lower); + try out.append(self.allocator, if (valid) lower else '_'); + } + if (out.items.len == 0 or !isIdentStart(out.items[0])) try out.insert(self.allocator, 0, '_'); + if (isReservedIdent(out.items)) try out.appendSlice(self.allocator, "_"); + return try out.toOwnedSlice(self.allocator); + } + + fn appendIndent(self: *UnifiedApiGenerator, indent: usize) !void { + for (0..indent) |_| try self.buffer.appendSlice(self.allocator, " "); } fn generateComments(self: *UnifiedApiGenerator, operation: Operation) !void { if (operation.summary) |summary| { try self.buffer.appendSlice(self.allocator, "/////////////////\n"); try self.buffer.appendSlice(self.allocator, "// Summary:\n"); - try self.buffer.appendSlice(self.allocator, "// "); - try self.buffer.appendSlice(self.allocator, summary); - try self.buffer.appendSlice(self.allocator, "\n"); + try self.appendLineComment(summary); try self.buffer.appendSlice(self.allocator, "//\n"); } if (operation.description) |description| { try self.buffer.appendSlice(self.allocator, "// Description:\n"); - try self.buffer.appendSlice(self.allocator, "// "); - try self.buffer.appendSlice(self.allocator, description); - try self.buffer.appendSlice(self.allocator, "\n"); + try self.appendLineComment(description); try self.buffer.appendSlice(self.allocator, "//\n"); } } @@ -86,105 +1245,128 @@ pub const UnifiedApiGenerator = struct { try self.buffer.appendSlice(self.allocator, "pub fn "); if (operation.operationId) |op_id| { - try self.buffer.appendSlice(self.allocator, op_id); + try self.appendIdentifier(op_id); } else { - try self.buffer.appendSlice(self.allocator, "operation"); - try self.buffer.appendSlice(self.allocator, path[1..]); // Remove leading slash + try self.buffer.appendSlice(self.allocator, "@\"operation"); + try self.buffer.appendSlice(self.allocator, path[1..]); + try self.buffer.appendSlice(self.allocator, "\""); } - try self.buffer.appendSlice(self.allocator, "(allocator: std.mem.Allocator, io: std.Io"); - var has_body_param = false; - var path_parameters = std.ArrayList([]const u8).empty; - defer path_parameters.deinit(self.allocator); + try self.buffer.appendSlice(self.allocator, "(client: *Client"); if (operation.parameters) |params| { - if (params.len > 0) try self.buffer.appendSlice(self.allocator, ", "); - var first = true; for (params) |param| { - if (!first) try self.buffer.appendSlice(self.allocator, ", "); - first = false; - var data_type: []const u8 = "[]const u8"; // Default to string - var name: []const u8 = param.name; + try self.buffer.appendSlice(self.allocator, ", "); + const name: []const u8 = if (param.location == .body) "requestBody" else param.name; + try self.appendIdentifier(name); + try self.buffer.appendSlice(self.allocator, ": "); if (param.location == .body) { - has_body_param = true; - name = "requestBody"; if (param.schema) |schema| { - if (schema.ref) |ref| { - if (std.mem.lastIndexOf(u8, ref, "/")) |last_slash| { - data_type = ref[last_slash + 1 ..]; - } - } else { - data_type = self.getZigTypeFromSchema(schema); - } - } - } else if (param.location == .path) { - try path_parameters.append(self.allocator, param.name); - if (param.type) |param_type| { - data_type = self.getZigTypeFromSchemaType(param_type); + try self.appendZigTypeFromSchema(schema); + } else { + try self.buffer.appendSlice(self.allocator, "std.json.Value"); } } else { - if (param.type) |param_type| { - data_type = self.getZigTypeFromSchemaType(param_type); + 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| { + try self.appendZigTypeFromSchemaType(param_type); + } else { + try self.buffer.appendSlice(self.allocator, "[]const u8"); } } - try self.buffer.appendSlice(self.allocator, name); - try self.buffer.appendSlice(self.allocator, ": "); - try self.buffer.appendSlice(self.allocator, data_type); } } - const return_type = self.getReturnType(method, operation); - try self.buffer.appendSlice(self.allocator, ") !"); - try self.buffer.appendSlice(self.allocator, return_type); - try self.buffer.appendSlice(self.allocator, " {\n"); + if (self.hasReturnValue(method, operation)) { + try self.buffer.appendSlice(self.allocator, ") !Owned("); + try self.appendReturnType(method, operation); + try self.buffer.appendSlice(self.allocator, ") {\n"); + } else { + try self.buffer.appendSlice(self.allocator, ") !void {\n"); + } } - fn getReturnType(self: *UnifiedApiGenerator, method: []const u8, operation: Operation) []const u8 { - if (std.mem.eql(u8, method, "GET")) { - if (operation.responses.get("200")) |path_item| { - if (path_item.schema) |schema| { - return self.getZigTypeFromSchema(schema); + fn generateFunctionBody(self: *UnifiedApiGenerator, method: []const u8, path: []const u8, operation: Operation) !void { + const operation_id = operation.operationId orelse return self.generateFunctionBodyDirect(method, path, operation); + if (self.hasReturnValue(method, operation)) { + const result_name = try std.fmt.allocPrint(self.allocator, "{s}Result", .{operation_id}); + defer self.allocator.free(result_name); + try self.buffer.appendSlice(self.allocator, " var result = try "); + try self.appendIdentifier(result_name); + try self.appendFlatCallArguments(operation); + try self.buffer.appendSlice(self.allocator, ";\n"); + try self.buffer.appendSlice(self.allocator, " switch (result) {\n"); + try self.buffer.appendSlice(self.allocator, " .ok => |ok| return ok,\n"); + try self.buffer.appendSlice(self.allocator, " .api_error => |*err| {\n"); + try self.buffer.appendSlice(self.allocator, " err.deinit();\n"); + try self.buffer.appendSlice(self.allocator, " return error.ResponseError;\n"); + try self.buffer.appendSlice(self.allocator, " },\n"); + try self.buffer.appendSlice(self.allocator, " .parse_error => |*err| {\n"); + try self.buffer.appendSlice(self.allocator, " err.raw.deinit();\n"); + try self.buffer.appendSlice(self.allocator, " return error.ResponseParseError;\n"); + try self.buffer.appendSlice(self.allocator, " },\n"); + try self.buffer.appendSlice(self.allocator, " }\n"); + } else { + const raw_name = try std.fmt.allocPrint(self.allocator, "{s}Raw", .{operation_id}); + defer self.allocator.free(raw_name); + try self.buffer.appendSlice(self.allocator, " var raw = try "); + try self.appendIdentifier(raw_name); + try self.appendFlatCallArguments(operation); + try self.buffer.appendSlice(self.allocator, ";\n"); + try self.buffer.appendSlice(self.allocator, " defer raw.deinit();\n"); + try self.buffer.appendSlice(self.allocator, " if (raw.status.class() != .success) return error.ResponseError;\n"); + } + try self.buffer.appendSlice(self.allocator, "}\n\n"); + } + + fn generateFunctionBodyDirect(self: *UnifiedApiGenerator, method: []const u8, path: []const u8, operation: Operation) !void { + try self.buffer.appendSlice(self.allocator, " const allocator = client.allocator;\n"); + + var has_body_param = false; + if (operation.parameters) |params| { + for (params) |param| { + if (param.location == .body) { + has_body_param = true; + break; } } } - return "void"; - } - - fn generateFunctionBody(self: *UnifiedApiGenerator, method: []const u8, path: []const u8, operation: Operation) !void { if (operation.parameters) |parameters| { - if (parameters.len > 0) { - for (parameters) |parameter| { - if (parameter.location != .path and parameter.location != .body) { - try self.buffer.appendSlice(self.allocator, " _ = "); - try self.buffer.appendSlice(self.allocator, parameter.name); - try self.buffer.appendSlice(self.allocator, ";\n"); - } + for (parameters) |parameter| { + if (parameter.location != .path and parameter.location != .body and parameter.location != .query) { + try self.buffer.appendSlice(self.allocator, " _ = "); + try self.appendIdentifier(parameter.name); + try self.buffer.appendSlice(self.allocator, ";\n"); } } } - try self.buffer.appendSlice(self.allocator, " var client: std.http.Client = .{ .allocator = allocator, .io = io };\n"); - try self.buffer.appendSlice(self.allocator, " defer client.deinit();\n\n"); + try self.buffer.appendSlice(self.allocator, " var headers = std.ArrayList(std.http.Header).empty;\n"); + try self.buffer.appendSlice(self.allocator, " defer headers.deinit(allocator);\n"); + try self.buffer.appendSlice(self.allocator, " const auth_header = try appendClientHeaders(allocator, &headers, client, "); + try self.buffer.appendSlice(self.allocator, if (has_body_param) "true" else "false"); + try self.buffer.appendSlice(self.allocator, ", \"application/json\");\n"); + try self.buffer.appendSlice(self.allocator, " defer if (auth_header) |value| allocator.free(value);\n\n"); - try self.buffer.appendSlice(self.allocator, " const headers = &[_]std.http.Header{\n"); - try self.buffer.appendSlice(self.allocator, " .{ .name = \"Content-Type\", .value = \"application/json\" },\n"); - try self.buffer.appendSlice(self.allocator, " .{ .name = \"Accept\", .value = \"application/json\" },\n"); - try self.buffer.appendSlice(self.allocator, " };\n"); - try self.buffer.appendSlice(self.allocator, "\n"); + var new_path = path; + var allocated_paths = std.ArrayList([]u8).empty; + defer { + for (allocated_paths.items) |allocated_path| self.allocator.free(allocated_path); + allocated_paths.deinit(self.allocator); + } if (operation.parameters) |parameters| { - var new_path = path; - var allocated_paths = std.ArrayList([]u8).empty; - defer { - for (allocated_paths.items) |allocated_path| { - self.allocator.free(allocated_path); - } - allocated_paths.deinit(self.allocator); - } - for (parameters) |parameter| { if (parameter.location != .path) continue; const param = parameter.name; - const param_type = switch (parameter.type orelse .string) { + const path_type = if (parameter.schema) |schema| + schema.type orelse .string + else + parameter.type orelse .string; + const param_type = switch (path_type) { .string => "s", .integer => "d", .number => "d", @@ -196,66 +1378,75 @@ pub const UnifiedApiGenerator = struct { _ = std.mem.replace(u8, new_path, param, param_type, output); new_path = output; } + } - try self.buffer.appendSlice(self.allocator, " const uri_str = try std.fmt.allocPrint(allocator, \""); - if (self.args.base_url) |base_url| { - try self.buffer.appendSlice(self.allocator, base_url); - } - try self.buffer.appendSlice(self.allocator, new_path); - try self.buffer.appendSlice(self.allocator, "\", .{"); - - var pos: i32 = 0; + try self.buffer.appendSlice(self.allocator, " var uri_buf: std.Io.Writer.Allocating = .init(allocator);\n"); + try self.buffer.appendSlice(self.allocator, " defer uri_buf.deinit();\n"); + try self.buffer.appendSlice(self.allocator, " try uri_buf.writer.print(\"{s}"); + try self.buffer.appendSlice(self.allocator, new_path); + try self.buffer.appendSlice(self.allocator, "\", .{client.base_url"); + if (operation.parameters) |parameters| { for (parameters) |parameter| { if (parameter.location != .path) continue; - const param = parameter.name; - try self.buffer.appendSlice(self.allocator, param); - pos += 1; - if (pos < parameters.len) - try self.buffer.appendSlice(self.allocator, ", "); - } - try self.buffer.appendSlice(self.allocator, "});\n"); - - try self.buffer.appendSlice(self.allocator, " defer allocator.free(uri_str);\n"); - try self.buffer.appendSlice(self.allocator, " const uri = try std.Uri.parse(uri_str);\n"); - } else { - try self.buffer.appendSlice(self.allocator, " const uri = try std.Uri.parse(\""); - if (self.args.base_url) |base_url| { - try self.buffer.appendSlice(self.allocator, base_url); + try self.buffer.appendSlice(self.allocator, ", "); + try self.appendIdentifier(parameter.name); } - try self.buffer.appendSlice(self.allocator, path); - try self.buffer.appendSlice(self.allocator, "\");\n"); } + try self.buffer.appendSlice(self.allocator, "});\n"); - var has_body_param = false; - if (operation.parameters) |params| { - for (params) |param| { - if (param.location == .body) { - has_body_param = true; + var has_query_param = false; + if (operation.parameters) |parameters| { + for (parameters) |parameter| { + if (parameter.location == .query) { + has_query_param = true; break; } } } + if (has_query_param) { + try self.buffer.appendSlice(self.allocator, " var first_query = true;\n"); + if (operation.parameters) |parameters| { + for (parameters) |parameter| { + if (parameter.location != .query) continue; + if (parameter.required) { + try self.buffer.appendSlice(self.allocator, " try appendQueryParam(&uri_buf.writer, &first_query, \""); + try self.buffer.appendSlice(self.allocator, parameter.name); + try self.buffer.appendSlice(self.allocator, "\", "); + try self.appendIdentifier(parameter.name); + try self.buffer.appendSlice(self.allocator, ");\n"); + } else { + try self.buffer.appendSlice(self.allocator, " if ("); + try self.appendIdentifier(parameter.name); + try self.buffer.appendSlice(self.allocator, ") |value| {\n"); + try self.buffer.appendSlice(self.allocator, " try appendQueryParam(&uri_buf.writer, &first_query, \""); + try self.buffer.appendSlice(self.allocator, parameter.name); + try self.buffer.appendSlice(self.allocator, "\", value);\n"); + try self.buffer.appendSlice(self.allocator, " }\n"); + } + } + } + } + try self.buffer.appendSlice(self.allocator, " const uri = try std.Uri.parse(uri_buf.written());\n"); if (has_body_param) { - try self.buffer.appendSlice(self.allocator, " var str: std.Io.Writer.Allocating = .init(allocator);\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\n"); + try self.buffer.appendSlice(self.allocator, " const payload = str.written();\n"); } - const return_type = self.getReturnType(method, operation); - const has_return_value = !std.mem.eql(u8, return_type, "void"); + const has_return_value = self.hasReturnValue(method, operation); if (has_return_value) { - try self.buffer.appendSlice(self.allocator, " var response_body: std.Io.Writer.Allocating = .init(allocator);\n"); - try self.buffer.appendSlice(self.allocator, " defer response_body.deinit();\n\n"); + try self.buffer.appendSlice(self.allocator, "\n var response_body: std.Io.Writer.Allocating = .init(allocator);\n"); + try self.buffer.appendSlice(self.allocator, " defer response_body.deinit();\n"); } - try self.buffer.appendSlice(self.allocator, " const result = try client.fetch(.{\n"); + try self.buffer.appendSlice(self.allocator, "\n const result = try client.http.fetch(.{\n"); try self.buffer.appendSlice(self.allocator, " .location = .{ .uri = uri },\n"); try self.buffer.appendSlice(self.allocator, " .method = std.http.Method."); try self.buffer.appendSlice(self.allocator, method); try self.buffer.appendSlice(self.allocator, ",\n"); - try self.buffer.appendSlice(self.allocator, " .extra_headers = headers,\n"); + try self.buffer.appendSlice(self.allocator, " .extra_headers = headers.items,\n"); if (has_body_param) { try self.buffer.appendSlice(self.allocator, " .payload = payload,\n"); } @@ -269,39 +1460,108 @@ pub const UnifiedApiGenerator = struct { if (has_return_value) { try self.buffer.appendSlice(self.allocator, "\n"); - try self.buffer.appendSlice(self.allocator, " const body = response_body.written();\n"); + try self.buffer.appendSlice(self.allocator, " const body = try response_body.toOwnedSlice();\n"); + try self.buffer.appendSlice(self.allocator, " errdefer allocator.free(body);\n"); try self.buffer.appendSlice(self.allocator, " const parsed = try std.json.parseFromSlice("); - try self.buffer.appendSlice(self.allocator, return_type); - try self.buffer.appendSlice(self.allocator, ", allocator, body, .{});\n"); - try self.buffer.appendSlice(self.allocator, " defer parsed.deinit();\n\n"); - try self.buffer.appendSlice(self.allocator, " return parsed.value;\n"); + try self.appendReturnType(method, operation); + try self.buffer.appendSlice(self.allocator, ", allocator, body, .{ .ignore_unknown_fields = true });\n"); + try self.buffer.appendSlice(self.allocator, " return .{ .allocator = allocator, .body = body, .parsed = parsed };\n"); } try self.buffer.appendSlice(self.allocator, "}\n\n"); } - fn getZigTypeFromSchema(self: *UnifiedApiGenerator, schema: Schema) []const u8 { + fn hasReturnValue(self: *UnifiedApiGenerator, method: []const u8, operation: Operation) bool { + _ = method; + return self.successResponseSchema(operation) != null; + } + + fn successResponseSchema(self: *UnifiedApiGenerator, operation: Operation) ?Schema { + _ = self; + const success_codes = [_][]const u8{ "200", "201", "202" }; + for (success_codes) |code| { + if (operation.responses.get(code)) |response| { + if (response.schema) |schema| return schema; + } + } + + var iterator = operation.responses.iterator(); + while (iterator.next()) |entry| { + const code = entry.key_ptr.*; + if (code.len == 3 and code[0] == '2') { + if (entry.value_ptr.schema) |schema| return schema; + } + } + + return null; + } + + fn appendReturnType(self: *UnifiedApiGenerator, method: []const u8, operation: Operation) !void { + _ = method; + if (self.successResponseSchema(operation)) |schema| { + try self.appendZigTypeFromSchema(schema); + return; + } + try self.buffer.appendSlice(self.allocator, "void"); + } + + fn appendZigQueryTypeFromSchema(self: *UnifiedApiGenerator, schema: Schema) !void { + if (schema.type) |schema_type| { + switch (schema_type) { + .string => try self.buffer.appendSlice(self.allocator, "[]const u8"), + .integer => try self.buffer.appendSlice(self.allocator, "i64"), + .number => try self.buffer.appendSlice(self.allocator, "f64"), + .boolean => try self.buffer.appendSlice(self.allocator, "bool"), + else => try self.buffer.appendSlice(self.allocator, "[]const u8"), + } + return; + } + try self.buffer.appendSlice(self.allocator, "[]const u8"); + } + + fn appendZigTypeFromSchema(self: *UnifiedApiGenerator, schema: Schema) !void { + if (schema.discriminator_property == null) { + const variants = schema.one_of orelse schema.any_of orelse &.{}; + if (variants.len == 2) { + var null_count: usize = 0; + var child: ?Schema = null; + for (variants) |variant| { + if (variant.type == .null) { + null_count += 1; + } else { + child = variant; + } + } + if (null_count == 1 and child != null) { + try self.buffer.appendSlice(self.allocator, "?"); + try self.appendZigTypeFromSchema(child.?); + return; + } + } + } + if (schema.ref) |ref| { if (std.mem.lastIndexOf(u8, ref, "/")) |last_slash| { - return ref[last_slash + 1 ..]; + try self.appendIdentifier(ref[last_slash + 1 ..]); + return; } } if (schema.type) |schema_type| { - return self.getZigTypeFromSchemaType(schema_type); + try self.appendZigTypeFromSchemaType(schema_type); + return; } - return "[]const u8"; // default fallback + try self.buffer.appendSlice(self.allocator, "std.json.Value"); } - fn getZigTypeFromSchemaType(self: *UnifiedApiGenerator, schema_type: SchemaType) []const u8 { - _ = self; - return switch (schema_type) { + fn appendZigTypeFromSchemaType(self: *UnifiedApiGenerator, schema_type: SchemaType) !void { + try self.buffer.appendSlice(self.allocator, switch (schema_type) { .string => "[]const u8", .integer => "i64", .number => "f64", .boolean => "bool", - .array => "[]const u8", // Simplified for now - .object => "std.json.Value", - .reference => "[]const u8", - }; + .array => "[]const std.json.Value", + .object, .reference => "std.json.Value", + .null => "void", + }); } }; diff --git a/src/generators/unified/model_generator.zig b/src/generators/unified/model_generator.zig index 839baf6..44fb066 100644 --- a/src/generators/unified/model_generator.zig +++ b/src/generators/unified/model_generator.zig @@ -1,11 +1,49 @@ const std = @import("std"); const UnifiedDocument = @import("../../models/common/document.zig").UnifiedDocument; const Schema = @import("../../models/common/document.zig").Schema; -const SchemaType = @import("../../models/common/document.zig").SchemaType; + +fn isIdentStart(c: u8) bool { + return std.ascii.isAlphabetic(c) or c == '_'; +} + +fn isIdentContinue(c: u8) bool { + return std.ascii.isAlphanumeric(c) or c == '_'; +} + +fn isReservedIdent(name: []const u8) bool { + const reserved = [_][]const u8{ + "addrspace", "align", "allowzero", "and", "anyerror", "anyframe", "anyopaque", "anytype", + "asm", "async", "await", "bool", "break", "callconv", "catch", "comptime", + "const", "continue", "defer", "else", "enum", "errdefer", "error", "export", + "extern", "false", "fn", "for", "if", "inline", "isize", "linksection", + "noalias", "noreturn", "nosuspend", "null", "opaque", "or", "orelse", "packed", + "pub", "resume", "return", "struct", "suspend", "switch", "test", "threadlocal", + "true", "try", "type", "undefined", "union", "unreachable", "usize", "usingnamespace", + "var", "void", "volatile", "while", + }; + for (reserved) |word| { + if (std.mem.eql(u8, name, word)) return true; + } + return false; +} + +fn isBareIdentifier(name: []const u8) bool { + if (name.len == 0 or !isIdentStart(name[0]) or isReservedIdent(name)) return false; + for (name[1..]) |c| { + if (!isIdentContinue(c)) return false; + } + return true; +} + +fn isExtensibleRequest(name: []const u8) bool { + return std.mem.eql(u8, name, "CreateResponse") or + std.mem.eql(u8, name, "CreateChatCompletionRequest"); +} pub const UnifiedModelGenerator = struct { allocator: std.mem.Allocator, buffer: std.ArrayList(u8), + source_schemas: ?*const std.StringHashMap(Schema) = null, pub fn init(allocator: std.mem.Allocator) UnifiedModelGenerator { return UnifiedModelGenerator{ @@ -24,11 +62,34 @@ pub const UnifiedModelGenerator = struct { if (document.schemas) |schemas| { try self.generateSchemas(schemas); + try self.generateManualAliases(schemas); } return try self.allocator.dupe(u8, self.buffer.items); } + fn appendIdentifier(self: *UnifiedModelGenerator, name: []const u8) !void { + if (isBareIdentifier(name)) { + try self.buffer.appendSlice(self.allocator, name); + return; + } + + try self.buffer.appendSlice(self.allocator, "@\""); + for (name) |c| { + switch (c) { + '\\', '"' => { + try self.buffer.append(self.allocator, '\\'); + try self.buffer.append(self.allocator, c); + }, + '\n' => try self.buffer.appendSlice(self.allocator, "\\n"), + '\r' => try self.buffer.appendSlice(self.allocator, "\\r"), + '\t' => try self.buffer.appendSlice(self.allocator, "\\t"), + else => try self.buffer.append(self.allocator, c), + } + } + try self.buffer.appendSlice(self.allocator, "\""); + } + fn generateHeader(self: *UnifiedModelGenerator) !void { try self.buffer.appendSlice(self.allocator, "const std = @import(\"std\");\n\n"); try self.buffer.appendSlice(self.allocator, "///////////////////////////////////////////\n"); @@ -37,6 +98,9 @@ pub const UnifiedModelGenerator = struct { } fn generateSchemas(self: *UnifiedModelGenerator, schemas: std.StringHashMap(Schema)) !void { + self.source_schemas = &schemas; + defer self.source_schemas = null; + var schema_iterator = schemas.iterator(); while (schema_iterator.next()) |entry| { const schema_name = entry.key_ptr.*; @@ -45,40 +109,1324 @@ pub const UnifiedModelGenerator = struct { } } - fn generateSchema(self: *UnifiedModelGenerator, name: []const u8, schema: Schema) !void { + fn generateSchema(self: *UnifiedModelGenerator, name: []const u8, schema: Schema) anyerror!void { + if (try self.generateManualSchema(name, schema)) return; if (schema.type == .reference) return; - try self.buffer.appendSlice(self.allocator, "pub const "); - try self.buffer.appendSlice(self.allocator, name); - try self.buffer.appendSlice(self.allocator, " = struct {\n"); + if (try self.generateUnionAlias(name, schema)) return; + if (try self.generateDiscriminatorUnion(name, schema)) return; + if (try self.generateStructuralUnion(name, schema)) return; + if (schema.description) |description| { + if (std.mem.eql(u8, description, "OpenAPI oneOf with discriminator could not be generated safely; generator currently uses std.json.Value.")) { + try self.buffer.appendSlice(self.allocator, "// OpenAPI oneOf with discriminator could not be generated safely; generator currently uses std.json.Value.\n"); + } + } if (schema.properties) |properties| { - try self.generateStructFields(properties, schema.required); + if (properties.count() > 0) { + try self.generateFieldHelpers(name, properties); + try self.buffer.appendSlice(self.allocator, "pub const "); + try self.appendIdentifier(name); + try self.buffer.appendSlice(self.allocator, " = struct {\n"); + try self.generateStructFields(name, properties, schema.required); + if (std.mem.eql(u8, name, "ChatCompletionRequestAssistantMessage") and !properties.contains("reasoning_details")) { + try self.buffer.appendSlice(self.allocator, " reasoning_details: ?std.json.Value = null,\n"); + } + if (isExtensibleRequest(name)) { + try self.buffer.appendSlice(self.allocator, " extra_body: ?std.json.Value = null,\n"); + try self.generateJsonStringify(properties, schema.required); + } + try self.buffer.appendSlice(self.allocator, "};\n\n"); + return; + } } - try self.buffer.appendSlice(self.allocator, "};\n\n"); + try self.buffer.appendSlice(self.allocator, "pub const "); + try self.appendIdentifier(name); + try self.buffer.appendSlice(self.allocator, " = "); + try self.appendZigType(schema); + try self.buffer.appendSlice(self.allocator, ";\n\n"); } - fn generateStructFields(self: *UnifiedModelGenerator, properties: std.StringHashMap(Schema), required: ?[][]const u8) !void { + fn appendStringLiteral(self: *UnifiedModelGenerator, value: []const u8) !void { + try self.buffer.append(self.allocator, '"'); + for (value) |c| { + switch (c) { + '\\', '"' => { + try self.buffer.append(self.allocator, '\\'); + try self.buffer.append(self.allocator, c); + }, + '\n' => try self.buffer.appendSlice(self.allocator, "\\n"), + '\r' => try self.buffer.appendSlice(self.allocator, "\\r"), + '\t' => try self.buffer.appendSlice(self.allocator, "\\t"), + else => try self.buffer.append(self.allocator, c), + } + } + try self.buffer.append(self.allocator, '"'); + } + + fn sanitizeIdentifierAlloc(self: *UnifiedModelGenerator, value: []const u8) ![]const u8 { + var out = std.ArrayList(u8).empty; + errdefer out.deinit(self.allocator); + var prev_was_underscore = false; + for (value, 0..) |c, i| { + const next = if (i + 1 < value.len) value[i + 1] else 0; + const prev = if (i > 0) value[i - 1] else 0; + const insert_word_break = std.ascii.isUpper(c) and i > 0 and out.items.len > 0 and !prev_was_underscore and + ((std.ascii.isLower(prev) or std.ascii.isDigit(prev)) or (std.ascii.isUpper(prev) and std.ascii.isLower(next))); + if (insert_word_break) try out.append(self.allocator, '_'); + + const lower = std.ascii.toLower(c); + const valid = if (out.items.len == 0) isIdentStart(lower) else isIdentContinue(lower); + const byte = if (valid) lower else '_'; + if (byte == '_' and prev_was_underscore) continue; + try out.append(self.allocator, byte); + prev_was_underscore = byte == '_'; + } + while (out.items.len > 0 and out.items[out.items.len - 1] == '_') _ = out.pop(); + if (out.items.len == 0 or !isIdentStart(out.items[0])) try out.insert(self.allocator, 0, '_'); + if (isReservedIdent(out.items)) try out.appendSlice(self.allocator, "_"); + return try out.toOwnedSlice(self.allocator); + } + + fn unionVariants(schema: Schema) ?[]Schema { + if (schema.one_of) |variants| return variants; + if (schema.any_of) |variants| return variants; + return null; + } + + fn isNullSchema(schema: Schema) bool { + return schema.type == .null; + } + + fn nonNullUnionChild(schema: Schema) ?Schema { + const variants = unionVariants(schema) orelse return null; + var child: ?Schema = null; + for (variants) |variant| { + if (isNullSchema(variant)) continue; + if (child != null) return null; + child = variant; + } + return child; + } + + fn isStringLikeSchema(self: *UnifiedModelGenerator, schema: Schema) bool { + if (schema.ref) |ref| { + const schemas = self.source_schemas orelse return false; + return self.isStringLikeSchema(schemas.get(refName(ref)) orelse return false); + } + if (schema.type == .string) return true; + if (unionVariants(schema)) |variants| { + for (variants) |variant| { + if (isNullSchema(variant)) continue; + if (!self.isStringLikeSchema(variant)) return false; + } + return true; + } + return false; + } + + fn isNullableSchema(schema: Schema) bool { + const variants = unionVariants(schema) orelse return false; + for (variants) |variant| if (isNullSchema(variant)) return true; + return false; + } + + fn isPrimitiveUnionSchema(schema: Schema) bool { + const variants = unionVariants(schema) orelse return false; + if (variants.len == 0) return false; + for (variants) |variant| { + if (isNullSchema(variant)) continue; + if (variant.ref != null or variant.properties != null or variant.items != null) return false; + switch (variant.type orelse return false) { + .string, .integer, .number, .boolean => {}, + else => return false, + } + } + return true; + } + + fn schemaVariantTag(self: *UnifiedModelGenerator, schema: Schema, discriminator_property: []const u8) ?[]const u8 { + const target = if (schema.ref) |ref| blk: { + const schemas = self.source_schemas orelse return null; + break :blk schemas.get(refName(ref)) orelse return null; + } else schema; + const properties = target.properties orelse return null; + const field = properties.get(discriminator_property) orelse return null; + const enum_values = field.enum_values orelse return null; + if (enum_values.len == 0) return null; + return switch (enum_values[0]) { + .string => |value| value, + else => null, + }; + } + + fn refName(ref: []const u8) []const u8 { + if (std.mem.lastIndexOf(u8, ref, "/")) |last_slash| return ref[last_slash + 1 ..]; + return ref; + } + + fn variantTypeNameAlloc(self: *UnifiedModelGenerator, union_name: []const u8, variant: Schema, index: usize) ![]const u8 { + if (variant.ref) |ref| return try self.allocator.dupe(u8, refName(ref)); + return try std.fmt.allocPrint(self.allocator, "{s}Variant{d}", .{ union_name, index }); + } + + fn appendTitleIdentPart(self: *UnifiedModelGenerator, out: *std.ArrayList(u8), value: []const u8) !void { + var capitalize_next = true; + for (value, 0..) |c, i| { + if (std.ascii.isAlphanumeric(c)) { + const prev = if (i > 0) value[i - 1] else 0; + const camel_boundary = std.ascii.isUpper(c) and i > 0 and (std.ascii.isLower(prev) or std.ascii.isDigit(prev)); + const lower = std.ascii.toLower(c); + try out.append(self.allocator, if (capitalize_next or camel_boundary) std.ascii.toUpper(lower) else lower); + capitalize_next = false; + } else { + capitalize_next = true; + } + } + } + + fn fieldTypeNameAlloc(self: *UnifiedModelGenerator, owner_name: []const u8, field_name: []const u8) ![]const u8 { + var out = std.ArrayList(u8).empty; + errdefer out.deinit(self.allocator); + try self.appendTitleIdentPart(&out, owner_name); + try self.appendTitleIdentPart(&out, field_name); + if (out.items.len == 0 or !isIdentStart(out.items[0])) try out.insert(self.allocator, 0, '_'); + return try out.toOwnedSlice(self.allocator); + } + + fn arrayFieldItemTypeNameAlloc(self: *UnifiedModelGenerator, owner_name: []const u8, field_name: []const u8) ![]const u8 { + const field_type_name = try self.fieldTypeNameAlloc(owner_name, field_name); + defer self.allocator.free(field_type_name); + return try std.fmt.allocPrint(self.allocator, "{s}Item", .{field_type_name}); + } + + fn discriminatorVariantsAreSafe(self: *UnifiedModelGenerator, variants: []Schema, discriminator_property: []const u8) !bool { + var names = std.StringHashMap(void).init(self.allocator); + defer { + var iterator = names.iterator(); + while (iterator.next()) |entry| self.allocator.free(entry.key_ptr.*); + names.deinit(); + } + + for (variants) |variant| { + const tag = self.schemaVariantTag(variant, discriminator_property) orelse return false; + if (variant.ref == null and (variant.properties == null or variant.properties.?.count() == 0)) return false; + const name = try self.sanitizeIdentifierAlloc(tag); + errdefer self.allocator.free(name); + if (std.mem.eql(u8, name, "raw") or names.contains(name)) { + self.allocator.free(name); + return false; + } + try names.put(name, {}); + } + return true; + } + + fn generateInlineVariantTypes(self: *UnifiedModelGenerator, union_name: []const u8, variants: []Schema) !void { + for (variants, 0..) |variant, i| { + if (variant.ref != null) continue; + const type_name = try self.variantTypeNameAlloc(union_name, variant, i); + defer self.allocator.free(type_name); + if (variant.type == .array) { + if (variant.items) |item_schema| { + if (try self.canGenerateNamedArrayItemType(item_schema.*)) { + const item_type_name = try std.fmt.allocPrint(self.allocator, "{s}Item", .{type_name}); + defer self.allocator.free(item_type_name); + try self.generateSchema(item_type_name, item_schema.*); + } + } + } + if (variant.properties) |properties| { + if (properties.count() > 0) { + try self.generateFieldHelpers(type_name, properties); + try self.buffer.appendSlice(self.allocator, "pub const "); + try self.appendIdentifier(type_name); + try self.buffer.appendSlice(self.allocator, " = struct {\n"); + try self.generateStructFields(type_name, properties, variant.required); + try self.buffer.appendSlice(self.allocator, "};\n\n"); + continue; + } + } + try self.buffer.appendSlice(self.allocator, "pub const "); + try self.appendIdentifier(type_name); + try self.buffer.appendSlice(self.allocator, " = "); + try self.appendZigType(variant); + try self.buffer.appendSlice(self.allocator, ";\n\n"); + } + } + + fn generateDiscriminatorUnion(self: *UnifiedModelGenerator, name: []const u8, schema: Schema) !bool { + const variants = unionVariants(schema) orelse return false; + const discriminator_property = schema.discriminator_property orelse return false; + if (!try self.discriminatorVariantsAreSafe(variants, discriminator_property)) return false; + + try self.generateInlineVariantTypes(name, variants); + + try self.buffer.appendSlice(self.allocator, "pub const "); + try self.appendIdentifier(name); + try self.buffer.appendSlice(self.allocator, " = union(enum) {\n"); + + for (variants, 0..) |variant, i| { + const tag = self.schemaVariantTag(variant, discriminator_property).?; + const field_name = try self.sanitizeIdentifierAlloc(tag); + defer self.allocator.free(field_name); + const type_name = try self.variantTypeNameAlloc(name, variant, i); + defer self.allocator.free(type_name); + try self.buffer.appendSlice(self.allocator, " "); + try self.buffer.appendSlice(self.allocator, field_name); + try self.buffer.appendSlice(self.allocator, ": "); + try self.appendIdentifier(type_name); + try self.buffer.appendSlice(self.allocator, ",\n"); + } + + try self.buffer.appendSlice(self.allocator, + \\ raw: std.json.Value, + \\ + \\ pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { + \\ const value = try std.json.innerParse(std.json.Value, allocator, source, options); + \\ return jsonParseFromValue(allocator, value, options); + \\ } + \\ + \\ pub fn jsonParseFromValue(allocator: std.mem.Allocator, source: std.json.Value, options: std.json.ParseOptions) !@This() { + \\ if (source != .object) return error.UnexpectedToken; + ); + try self.buffer.appendSlice(self.allocator, " const discriminator = source.object.get("); + try self.appendStringLiteral(discriminator_property); + try self.buffer.appendSlice(self.allocator, + \\) orelse return .{ .raw = source }; + \\ if (discriminator != .string) return .{ .raw = source }; + \\ + ); + + for (variants, 0..) |variant, i| { + const tag = self.schemaVariantTag(variant, discriminator_property).?; + const field_name = try self.sanitizeIdentifierAlloc(tag); + defer self.allocator.free(field_name); + const type_name = try self.variantTypeNameAlloc(name, variant, i); + defer self.allocator.free(type_name); + try self.buffer.appendSlice(self.allocator, " if (std.mem.eql(u8, discriminator.string, "); + try self.appendStringLiteral(tag); + try self.buffer.appendSlice(self.allocator, ")) {\n"); + try self.buffer.appendSlice(self.allocator, " return .{ ."); + try self.buffer.appendSlice(self.allocator, field_name); + try self.buffer.appendSlice(self.allocator, " = try std.json.parseFromValueLeaky("); + try self.appendIdentifier(type_name); + try self.buffer.appendSlice(self.allocator, ", allocator, source, options) };\n"); + try self.buffer.appendSlice(self.allocator, " }\n"); + } + + try self.buffer.appendSlice(self.allocator, + \\ + \\ return .{ .raw = source }; + \\ } + \\ + \\ pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void { + \\ switch (self) { + ); + + for (variants) |variant| { + const tag = self.schemaVariantTag(variant, discriminator_property).?; + const field_name = try self.sanitizeIdentifierAlloc(tag); + defer self.allocator.free(field_name); + try self.buffer.appendSlice(self.allocator, " ."); + try self.buffer.appendSlice(self.allocator, field_name); + try self.buffer.appendSlice(self.allocator, " => |value| try jw.write(value),\n"); + } + + try self.buffer.appendSlice(self.allocator, + \\ .raw => |value| try jw.write(value), + \\ } + \\ } + \\}; + \\ + \\ + ); + return true; + } + + fn variantFieldNameAlloc(self: *UnifiedModelGenerator, variant: Schema, index: usize) ![]const u8 { + if (self.schemaVariantTag(variant, "event")) |tag| return try self.sanitizeIdentifierAlloc(tag); + if (self.schemaVariantTag(variant, "type")) |tag| return try self.sanitizeIdentifierAlloc(tag); + if (variant.ref) |ref| return try self.sanitizeIdentifierAlloc(refName(ref)); + if (variant.title) |title| { + if (std.ascii.indexOfIgnoreCase(title, "text") != null and variant.type == .string) return try self.allocator.dupe(u8, "text"); + return try self.sanitizeIdentifierAlloc(title); + } + if (variant.type) |schema_type| { + return try self.allocator.dupe(u8, switch (schema_type) { + .string => "string", + .integer => "integer", + .number => "number", + .boolean => "boolean", + .array => "items", + .object => "object", + else => "value", + }); + } + return try std.fmt.allocPrint(self.allocator, "variant_{d}", .{index}); + } + + fn resolvedSchema(self: *UnifiedModelGenerator, schema: Schema) ?Schema { + if (schema.ref) |ref| { + const schemas = self.source_schemas orelse return null; + return schemas.get(refName(ref)); + } + return schema; + } + + fn stringEnumValues(self: *UnifiedModelGenerator, schema: Schema) ?[]const std.json.Value { + const resolved = self.resolvedSchema(schema) orelse return null; + if (resolved.type != .string) return null; + const values = resolved.enum_values orelse return null; + if (values.len == 0) return null; + for (values) |value| if (value != .string) return null; + return values; + } + + fn arrayVariantFieldNameAlloc(self: *UnifiedModelGenerator, variant: Schema, index: usize) !?[]const u8 { + if (variant.type != .array) return null; + if (variant.title) |title| { + const name = try self.sanitizeIdentifierAlloc(title); + if (!std.mem.eql(u8, name, "array")) return name; + self.allocator.free(name); + } + const items = variant.items orelse return try std.fmt.allocPrint(self.allocator, "items_{d}", .{index}); + if (items.ref) |ref| { + const base = try self.sanitizeIdentifierAlloc(refName(ref)); + defer self.allocator.free(base); + return try std.fmt.allocPrint(self.allocator, "{s}_items", .{base}); + } + if (items.type) |item_type| { + switch (item_type) { + .string => return try self.allocator.dupe(u8, "strings"), + .integer => return try self.allocator.dupe(u8, "integers"), + .number => return try self.allocator.dupe(u8, "numbers"), + .boolean => return try self.allocator.dupe(u8, "booleans"), + .array => return try self.allocator.dupe(u8, "arrays"), + else => {}, + } + } + if (items.title) |title| { + const base = try self.sanitizeIdentifierAlloc(title); + defer self.allocator.free(base); + return try std.fmt.allocPrint(self.allocator, "{s}_items", .{base}); + } + return try std.fmt.allocPrint(self.allocator, "items_{d}", .{index}); + } + + fn structuralVariantFieldNameAlloc(self: *UnifiedModelGenerator, variant: Schema, index: usize) ![]const u8 { + if (variant.ref) |ref| return try self.sanitizeIdentifierAlloc(refName(ref)); + if (try self.arrayVariantFieldNameAlloc(variant, index)) |name| return name; + return self.variantFieldNameAlloc(variant, index); + } + + fn appendStructuralVariantType(self: *UnifiedModelGenerator, union_name: []const u8, variant: Schema, index: usize) !void { + if (variant.ref != null or variant.properties != null) { + const type_name = try self.variantTypeNameAlloc(union_name, variant, index); + defer self.allocator.free(type_name); + try self.appendIdentifier(type_name); + return; + } + if (variant.type == .array) { + if (variant.items) |item_schema| { + if (try self.canGenerateNamedArrayItemType(item_schema.*)) { + const type_name = try self.variantTypeNameAlloc(union_name, variant, index); + defer self.allocator.free(type_name); + const item_type_name = try std.fmt.allocPrint(self.allocator, "{s}Item", .{type_name}); + defer self.allocator.free(item_type_name); + try self.buffer.appendSlice(self.allocator, "[]const "); + try self.appendIdentifier(item_type_name); + return; + } + } + } + try self.appendZigType(variant); + } + + fn structuralUnionVariantsAreSafe(self: *UnifiedModelGenerator, variants: []Schema) !bool { + var names = std.StringHashMap(void).init(self.allocator); + defer { + var iterator = names.iterator(); + while (iterator.next()) |entry| self.allocator.free(entry.key_ptr.*); + names.deinit(); + } + for (variants, 0..) |variant, i| { + if (isNullSchema(variant)) return false; + if (self.stringEnumValues(variant)) |values| { + for (values) |value| { + const field_name = try self.sanitizeIdentifierAlloc(value.string); + errdefer self.allocator.free(field_name); + if (std.mem.eql(u8, field_name, "raw") or names.contains(field_name)) { + self.allocator.free(field_name); + return false; + } + try names.put(field_name, {}); + } + continue; + } + if (variant.ref == null and variant.type == null and variant.properties == null) return false; + const field_name = try self.structuralVariantFieldNameAlloc(variant, i); + errdefer self.allocator.free(field_name); + if (std.mem.eql(u8, field_name, "raw") or names.contains(field_name)) { + self.allocator.free(field_name); + return false; + } + try names.put(field_name, {}); + } + return true; + } + + fn generateStructuralUnion(self: *UnifiedModelGenerator, name: []const u8, schema: Schema) !bool { + const variants = unionVariants(schema) orelse return false; + if (!try self.structuralUnionVariantsAreSafe(variants)) return false; + + try self.generateInlineVariantTypes(name, variants); + + try self.buffer.appendSlice(self.allocator, "pub const "); + try self.appendIdentifier(name); + try self.buffer.appendSlice(self.allocator, " = union(enum) {\n"); + for (variants, 0..) |variant, i| { + if (self.stringEnumValues(variant)) |values| { + for (values) |value| { + const field_name = try self.sanitizeIdentifierAlloc(value.string); + defer self.allocator.free(field_name); + try self.buffer.appendSlice(self.allocator, " "); + try self.buffer.appendSlice(self.allocator, field_name); + try self.buffer.appendSlice(self.allocator, ",\n"); + } + continue; + } + const field_name = try self.structuralVariantFieldNameAlloc(variant, i); + defer self.allocator.free(field_name); + try self.buffer.appendSlice(self.allocator, " "); + try self.buffer.appendSlice(self.allocator, field_name); + try self.buffer.appendSlice(self.allocator, ": "); + try self.appendStructuralVariantType(name, variant, i); + try self.buffer.appendSlice(self.allocator, ",\n"); + } + + try self.buffer.appendSlice(self.allocator, + \\ raw: std.json.Value, + \\ + \\ pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { + \\ const value = try std.json.innerParse(std.json.Value, allocator, source, options); + \\ return jsonParseFromValue(allocator, value, options); + \\ } + \\ + \\ pub fn jsonParseFromValue(allocator: std.mem.Allocator, source: std.json.Value, options: std.json.ParseOptions) !@This() { + ); + for (variants) |variant| { + if (self.stringEnumValues(variant)) |values| { + for (values) |value| { + const field_name = try self.sanitizeIdentifierAlloc(value.string); + defer self.allocator.free(field_name); + try self.buffer.appendSlice(self.allocator, " if (source == .string and std.mem.eql(u8, source.string, "); + try self.appendStringLiteral(value.string); + try self.buffer.appendSlice(self.allocator, ")) return ."); + try self.buffer.appendSlice(self.allocator, field_name); + try self.buffer.appendSlice(self.allocator, ";\n"); + } + } + } + for (variants, 0..) |variant, i| { + if (self.stringEnumValues(variant) != null) continue; + const field_name = try self.structuralVariantFieldNameAlloc(variant, i); + defer self.allocator.free(field_name); + try self.buffer.appendSlice(self.allocator, " if (std.json.parseFromValueLeaky("); + try self.appendStructuralVariantType(name, variant, i); + try self.buffer.appendSlice(self.allocator, ", allocator, source, options)) |value| {\n"); + try self.buffer.appendSlice(self.allocator, " return .{ ."); + try self.buffer.appendSlice(self.allocator, field_name); + try self.buffer.appendSlice(self.allocator, " = value };\n"); + try self.buffer.appendSlice(self.allocator, " } else |_| {}\n"); + } + try self.buffer.appendSlice(self.allocator, + \\ return .{ .raw = source }; + \\ } + \\ + \\ pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void { + \\ switch (self) { + ); + for (variants, 0..) |variant, i| { + if (self.stringEnumValues(variant)) |values| { + for (values) |value| { + const field_name = try self.sanitizeIdentifierAlloc(value.string); + defer self.allocator.free(field_name); + try self.buffer.appendSlice(self.allocator, " ."); + try self.buffer.appendSlice(self.allocator, field_name); + try self.buffer.appendSlice(self.allocator, " => try jw.write("); + try self.appendStringLiteral(value.string); + try self.buffer.appendSlice(self.allocator, "),\n"); + } + continue; + } + const field_name = try self.structuralVariantFieldNameAlloc(variant, i); + defer self.allocator.free(field_name); + try self.buffer.appendSlice(self.allocator, " ."); + try self.buffer.appendSlice(self.allocator, field_name); + try self.buffer.appendSlice(self.allocator, " => |value| try jw.write(value),\n"); + } + try self.buffer.appendSlice(self.allocator, + \\ .raw => |value| try jw.write(value), + \\ } + \\ } + \\}; + \\ + \\ + ); + return true; + } + + fn generateUnionAlias(self: *UnifiedModelGenerator, name: []const u8, schema: Schema) !bool { + if (schema.discriminator_property != null) return false; + if (nonNullUnionChild(schema)) |child| { + const variants = unionVariants(schema).?; + var null_count: usize = 0; + for (variants) |variant| { + if (isNullSchema(variant)) null_count += 1; + } + if (null_count == 1 and variants.len == 2) { + try self.buffer.appendSlice(self.allocator, "pub const "); + try self.appendIdentifier(name); + try self.buffer.appendSlice(self.allocator, " = ?"); + try self.appendZigType(child); + try self.buffer.appendSlice(self.allocator, ";\n\n"); + return true; + } + } + if (self.isStringLikeSchema(schema)) { + try self.buffer.appendSlice(self.allocator, "pub const "); + try self.appendIdentifier(name); + try self.buffer.appendSlice(self.allocator, " = "); + if (isNullableSchema(schema)) try self.buffer.appendSlice(self.allocator, "?"); + try self.buffer.appendSlice(self.allocator, "[]const u8;\n\n"); + return true; + } + + if (!isPrimitiveUnionSchema(schema)) return false; + const variants = unionVariants(schema).?; + var has_string = false; + var has_integer = false; + var has_number = false; + var has_boolean = false; + for (variants) |variant| { + if (isNullSchema(variant)) continue; + switch (variant.type.?) { + .string => has_string = true, + .integer => has_integer = true, + .number => has_number = true, + .boolean => has_boolean = true, + else => {}, + } + } + const unique_count = @as(u8, @intFromBool(has_string)) + @as(u8, @intFromBool(has_integer)) + @as(u8, @intFromBool(has_number)) + @as(u8, @intFromBool(has_boolean)); + if (unique_count == 1) { + try self.buffer.appendSlice(self.allocator, "pub const "); + try self.appendIdentifier(name); + try self.buffer.appendSlice(self.allocator, " = "); + if (has_string) try self.buffer.appendSlice(self.allocator, "[]const u8") else if (has_integer) try self.buffer.appendSlice(self.allocator, "i64") else if (has_number) try self.buffer.appendSlice(self.allocator, "f64") else try self.buffer.appendSlice(self.allocator, "bool"); + try self.buffer.appendSlice(self.allocator, ";\n\n"); + return true; + } + + try self.buffer.appendSlice(self.allocator, "pub const "); + try self.appendIdentifier(name); + try self.buffer.appendSlice(self.allocator, " = union(enum) {\n"); + if (has_string) try self.buffer.appendSlice(self.allocator, " string: []const u8,\n"); + if (has_integer) try self.buffer.appendSlice(self.allocator, " integer: i64,\n"); + if (has_number) try self.buffer.appendSlice(self.allocator, " number: f64,\n"); + if (has_boolean) try self.buffer.appendSlice(self.allocator, " boolean: bool,\n"); + try self.buffer.appendSlice(self.allocator, + \\ raw: std.json.Value, + \\ + \\ pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { + \\ const value = try std.json.innerParse(std.json.Value, allocator, source, options); + \\ return jsonParseFromValue(allocator, value, options); + \\ } + \\ + \\ pub fn jsonParseFromValue(_: std.mem.Allocator, source: std.json.Value, _: std.json.ParseOptions) !@This() { + \\ return switch (source) { + ); + var emitted_string = false; + var emitted_integer = false; + var emitted_number = false; + var emitted_boolean = false; + for (variants) |variant| { + if (isNullSchema(variant)) continue; + switch (variant.type.?) { + .string => if (!emitted_string) { + emitted_string = true; + try self.buffer.appendSlice(self.allocator, " .string => |value| .{ .string = value },\n"); + }, + .integer => if (!emitted_integer) { + emitted_integer = true; + try self.buffer.appendSlice(self.allocator, " .integer => |value| .{ .integer = value },\n"); + }, + .number => if (!emitted_number) { + emitted_number = true; + try self.buffer.appendSlice(self.allocator, " .float => |value| .{ .number = value },\n"); + }, + .boolean => if (!emitted_boolean) { + emitted_boolean = true; + try self.buffer.appendSlice(self.allocator, " .bool => |value| .{ .boolean = value },\n"); + }, + else => {}, + } + } + try self.buffer.appendSlice(self.allocator, + \\ else => .{ .raw = source }, + \\ }; + \\ } + \\ + \\ pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void { + \\ switch (self) { + ); + if (has_string) try self.buffer.appendSlice(self.allocator, " .string => |value| try jw.write(value),\n"); + if (has_integer) try self.buffer.appendSlice(self.allocator, " .integer => |value| try jw.write(value),\n"); + if (has_number) try self.buffer.appendSlice(self.allocator, " .number => |value| try jw.write(value),\n"); + if (has_boolean) try self.buffer.appendSlice(self.allocator, " .boolean => |value| try jw.write(value),\n"); + try self.buffer.appendSlice(self.allocator, + \\ .raw => |value| try jw.write(value), + \\ } + \\ } + \\}; + \\ + \\ + ); + return true; + } + + fn appendJsonValueBackedUnionType(self: *UnifiedModelGenerator, name: []const u8) !void { + try self.buffer.appendSlice(self.allocator, "pub const "); + try self.appendIdentifier(name); + try self.buffer.appendSlice(self.allocator, + \\ = union(enum) { + \\ null, + \\ bool: bool, + \\ integer: i64, + \\ float: f64, + \\ number_string: []const u8, + \\ string: []const u8, + \\ array: std.json.Array, + \\ object: std.json.ObjectMap, + \\ + \\ pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { + \\ const value = try std.json.innerParse(std.json.Value, allocator, source, options); + \\ return jsonParseFromValue(allocator, value, options); + \\ } + \\ + \\ pub fn jsonParseFromValue(_: std.mem.Allocator, source: std.json.Value, _: std.json.ParseOptions) !@This() { + \\ return switch (source) { + \\ .null => .null, + \\ .bool => |value| .{ .bool = value }, + \\ .integer => |value| .{ .integer = value }, + \\ .float => |value| .{ .float = value }, + \\ .number_string => |value| .{ .number_string = value }, + \\ .string => |value| .{ .string = value }, + \\ .array => |value| .{ .array = value }, + \\ .object => |value| .{ .object = value }, + \\ }; + \\ } + \\ + \\ pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void { + \\ switch (self) { + \\ .null => try jw.write(null), + \\ .bool => |value| try jw.write(value), + \\ .integer => |value| try jw.write(value), + \\ .float => |value| try jw.write(value), + \\ .number_string => |value| try jw.print("{s}", .{value}), + \\ .string => |value| try jw.write(value), + \\ .array => |value| try jw.write(value.items), + \\ .object => |value| { + \\ try jw.beginObject(); + \\ var iterator = value.iterator(); + \\ while (iterator.next()) |entry| { + \\ try jw.objectField(entry.key_ptr.*); + \\ try jw.write(entry.value_ptr.*); + \\ } + \\ try jw.endObject(); + \\ }, + \\ } + \\ } + \\}; + \\ + \\ + ); + } + + fn generateManualSchema(self: *UnifiedModelGenerator, name: []const u8, schema: Schema) !bool { + _ = schema; + if (std.mem.eql(u8, name, "EmptyModelParam")) { + try self.buffer.appendSlice(self.allocator, + \\pub const EmptyModelParam = struct { + \\ pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { + \\ _ = try std.json.innerParse(std.json.Value, allocator, source, options); + \\ return .{}; + \\ } + \\ + \\ pub fn jsonParseFromValue(_: std.mem.Allocator, _: std.json.Value, _: std.json.ParseOptions) !@This() { + \\ return .{}; + \\ } + \\ + \\ pub fn jsonStringify(_: @This(), jw: *std.json.Stringify) !void { + \\ try jw.beginObject(); + \\ try jw.endObject(); + \\ } + \\}; + \\ + \\ + ); + return true; + } + + if (std.mem.eql(u8, name, "FunctionParameters") or std.mem.eql(u8, name, "ResponseFormatJsonSchemaSchema")) { + try self.appendJsonValueBackedUnionType(name); + return true; + } + + if (std.mem.eql(u8, name, "CompoundFilter")) { + try self.buffer.appendSlice(self.allocator, + \\pub const CompoundFilterItem = union(enum) { + \\ comparison_filter: ComparisonFilter, + \\ compound_filter: CompoundFilter, + \\ raw: std.json.Value, + \\ + \\ pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { + \\ const value = try std.json.innerParse(std.json.Value, allocator, source, options); + \\ return jsonParseFromValue(allocator, value, options); + \\ } + \\ + \\ pub fn jsonParseFromValue(allocator: std.mem.Allocator, source: std.json.Value, options: std.json.ParseOptions) !@This() { + \\ if (source != .object) return .{ .raw = source }; + \\ const discriminator = source.object.get("type") orelse return .{ .raw = source }; + \\ if (discriminator != .string) return .{ .raw = source }; + \\ if (std.mem.eql(u8, discriminator.string, "and") or std.mem.eql(u8, discriminator.string, "or")) { + \\ if (std.json.parseFromValueLeaky(CompoundFilter, allocator, source, options)) |value| return .{ .compound_filter = value } else |_| return .{ .raw = source }; + \\ } + \\ if (std.mem.eql(u8, discriminator.string, "eq") or std.mem.eql(u8, discriminator.string, "ne") or std.mem.eql(u8, discriminator.string, "gt") or std.mem.eql(u8, discriminator.string, "gte") or std.mem.eql(u8, discriminator.string, "lt") or std.mem.eql(u8, discriminator.string, "lte") or std.mem.eql(u8, discriminator.string, "in") or std.mem.eql(u8, discriminator.string, "nin")) { + \\ if (std.json.parseFromValueLeaky(ComparisonFilter, allocator, source, options)) |value| return .{ .comparison_filter = value } else |_| return .{ .raw = source }; + \\ } + \\ return .{ .raw = source }; + \\ } + \\ + \\ pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void { + \\ switch (self) { + \\ .comparison_filter => |value| try jw.write(value), + \\ .compound_filter => |value| try jw.write(value), + \\ .raw => |value| try jw.write(value), + \\ } + \\ } + \\}; + \\ + \\pub const CompoundFilter = struct { + \\ type: []const u8, + \\ filters: []const CompoundFilterItem, + \\}; + \\ + \\ + ); + return true; + } + + if (std.mem.eql(u8, name, "ToolChoiceAllowed")) { + try self.buffer.appendSlice(self.allocator, + \\pub const ToolChoiceAllowed = struct { + \\ type: []const u8, + \\ mode: []const u8, + \\ tools: []const Tool, + \\}; + \\ + \\ + ); + return true; + } + + if (std.mem.eql(u8, name, "ChatCompletionAllowedTools")) { + try self.buffer.appendSlice(self.allocator, + \\pub const ChatCompletionAllowedTools = struct { + \\ mode: []const u8, + \\ tools: []const ChatCompletionTool, + \\}; + \\ + \\ + ); + return true; + } + + if (std.mem.eql(u8, name, "CreateChatCompletionResponse")) { + try self.buffer.appendSlice(self.allocator, + \\pub const CreateChatCompletionResponse = struct { + \\ id: []const u8, + \\ object: []const u8, + \\ created: i64, + \\ model: []const u8, + \\ choices: []const ChatCompletionChoice, + \\ usage: ?CompletionUsage = null, + \\ system_fingerprint: ?[]const u8 = null, + \\ service_tier: ?ServiceTier = null, + \\}; + \\ + \\pub const ChatCompletionChoice = struct { + \\ index: i64, + \\ message: ChatCompletionResponseMessage, + \\ finish_reason: ?[]const u8 = null, + \\ logprobs: ?std.json.Value = null, + \\}; + \\ + \\ + ); + return true; + } + + if (std.mem.eql(u8, name, "CreateChatCompletionStreamResponse")) { + try self.buffer.appendSlice(self.allocator, + \\pub const CreateChatCompletionStreamResponse = struct { + \\ id: []const u8, + \\ object: []const u8, + \\ created: i64, + \\ model: []const u8, + \\ choices: []const ChatCompletionChunkChoice, + \\ usage: ?CompletionUsage = null, + \\ system_fingerprint: ?[]const u8 = null, + \\ service_tier: ?ServiceTier = null, + \\}; + \\ + \\pub const ChatCompletionChunkChoice = struct { + \\ index: i64, + \\ delta: ChatCompletionStreamResponseDelta, + \\ finish_reason: ?[]const u8 = null, + \\ logprobs: ?std.json.Value = null, + \\}; + \\ + \\ + ); + return true; + } + + if (std.mem.eql(u8, name, "ChatCompletionResponseMessage")) { + try self.buffer.appendSlice(self.allocator, + \\pub const ChatCompletionResponseMessageUrlCitation = struct { + \\ end_index: i64, + \\ start_index: i64, + \\ url: []const u8, + \\ title: []const u8, + \\}; + \\ + \\pub const ChatCompletionResponseMessageAnnotation = struct { + \\ type: []const u8, + \\ url_citation: ChatCompletionResponseMessageUrlCitation, + \\}; + \\ + \\pub const ChatCompletionResponseMessage = struct { + \\ role: []const u8, + \\ content: ?[]const u8 = null, + \\ refusal: ?[]const u8 = null, + \\ tool_calls: ?[]const ChatCompletionMessageToolCall = null, + \\ reasoning_details: ?std.json.Value = null, + \\ annotations: ?[]const ChatCompletionResponseMessageAnnotation = null, + \\ function_call: ?std.json.Value = null, + \\ audio: ?ChatCompletionResponseMessageAudio = null, + \\}; + \\ + \\ + ); + return true; + } + + if (std.mem.eql(u8, name, "ChatCompletionStreamResponseDelta")) { + try self.buffer.appendSlice(self.allocator, + \\pub const ChatCompletionStreamResponseDelta = struct { + \\ role: ?[]const u8 = null, + \\ content: ?[]const u8 = null, + \\ refusal: ?[]const u8 = null, + \\ tool_calls: ?[]const ChatCompletionMessageToolCallChunk = null, + \\ function_call: ?std.json.Value = null, + \\}; + \\ + \\ + ); + return true; + } + + if (std.mem.eql(u8, name, "InputMessageContent")) { + try self.buffer.appendSlice(self.allocator, + \\pub const InputMessageContent = union(enum) { + \\ text: []const u8, + \\ parts: []const InputContent, + \\ raw: std.json.Value, + \\ + \\ pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { + \\ const value = try std.json.innerParse(std.json.Value, allocator, source, options); + \\ return jsonParseFromValue(allocator, value, options); + \\ } + \\ + \\ pub fn jsonParseFromValue(allocator: std.mem.Allocator, source: std.json.Value, options: std.json.ParseOptions) !@This() { + \\ return switch (source) { + \\ .string => |value| .{ .text = value }, + \\ .array => .{ .parts = try std.json.parseFromValueLeaky([]const InputContent, allocator, source, options) }, + \\ else => .{ .raw = source }, + \\ }; + \\ } + \\ + \\ pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void { + \\ switch (self) { + \\ .text => |value| try jw.write(value), + \\ .parts => |value| try jw.write(value), + \\ .raw => |value| try jw.write(value), + \\ } + \\ } + \\}; + \\ + \\ + ); + return true; + } + + if (std.mem.eql(u8, name, "EasyInputMessage")) { + try self.buffer.appendSlice(self.allocator, + \\pub const EasyInputMessage = struct { + \\ role: []const u8, + \\ content: InputMessageContent, + \\ phase: ?MessagePhase = null, + \\ type: ?[]const u8 = null, + \\}; + \\ + \\ + ); + return true; + } + + if (std.mem.eql(u8, name, "ChatCompletionRequestMessageContentPartImage")) { + try self.buffer.appendSlice(self.allocator, + \\pub const ChatCompletionRequestMessageContentPartImageUrl = struct { + \\ url: []const u8, + \\ detail: ?[]const u8 = null, + \\}; + \\ + \\pub const ChatCompletionRequestMessageContentPartImage = struct { + \\ type: []const u8, + \\ image_url: ChatCompletionRequestMessageContentPartImageUrl, + \\}; + \\ + \\ + ); + return true; + } + + if (std.mem.eql(u8, name, "ChatCompletionRequestMessageContentPartFile")) { + try self.buffer.appendSlice(self.allocator, + \\pub const ChatCompletionRequestMessageContentPartFileData = struct { + \\ filename: ?[]const u8 = null, + \\ file_data: ?[]const u8 = null, + \\ file_id: ?[]const u8 = null, + \\}; + \\ + \\pub const ChatCompletionRequestMessageContentPartFile = struct { + \\ type: []const u8, + \\ file: ChatCompletionRequestMessageContentPartFileData, + \\}; + \\ + \\ + ); + return true; + } + + if (std.mem.eql(u8, name, "ChatCompletionRequestMessage")) { + try self.buffer.appendSlice(self.allocator, + \\pub const ChatCompletionRequestMessage = struct { + \\ role: []const u8, + \\ content: ?std.json.Value = null, + \\ name: ?[]const u8 = null, + \\ tool_calls: ?[]const ChatCompletionMessageToolCall = null, + \\ tool_call_id: ?[]const u8 = null, + \\ refusal: ?std.json.Value = null, + \\ reasoning_details: ?std.json.Value = null, + \\ extra: ?std.json.Value = null, + \\ + \\ pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void { + \\ try jw.beginObject(); + \\ try jw.objectField("role"); + \\ try jw.write(self.role); + \\ if (self.content) |value| { + \\ try jw.objectField("content"); + \\ try jw.write(value); + \\ } + \\ if (self.name) |value| { + \\ try jw.objectField("name"); + \\ try jw.write(value); + \\ } + \\ if (self.tool_calls) |value| { + \\ try jw.objectField("tool_calls"); + \\ try jw.write(value); + \\ } + \\ if (self.tool_call_id) |value| { + \\ try jw.objectField("tool_call_id"); + \\ try jw.write(value); + \\ } + \\ if (self.refusal) |value| { + \\ try jw.objectField("refusal"); + \\ try jw.write(value); + \\ } + \\ if (self.reasoning_details) |value| { + \\ try jw.objectField("reasoning_details"); + \\ try jw.write(value); + \\ } + \\ if (self.extra) |extra| { + \\ if (extra == .object) { + \\ var iterator = extra.object.iterator(); + \\ while (iterator.next()) |entry| { + \\ try jw.objectField(entry.key_ptr.*); + \\ try jw.write(entry.value_ptr.*); + \\ } + \\ } + \\ } + \\ try jw.endObject(); + \\ } + \\}; + \\ + \\ + ); + return true; + } + + if (std.mem.eql(u8, name, "ChatCompletionMessageToolCalls")) { + try self.buffer.appendSlice(self.allocator, "pub const ChatCompletionMessageToolCalls = []const ChatCompletionMessageToolCall;\n\n"); + return true; + } + + return false; + } + + fn generateOpenAiDynamicFieldTypes(self: *UnifiedModelGenerator) !void { + try self.buffer.appendSlice(self.allocator, + \\pub const OpenApi2ZigDynamicObject = std.json.ArrayHashMap(std.json.Value); + \\ + \\pub const EvalResponsesSourceMetadata = OpenApi2ZigDynamicObject; + \\pub const EvalRunOutputItemResultSample = OpenApi2ZigDynamicObject; + \\pub const AssignedRoleDetailsCreatedByUserObj = OpenApi2ZigDynamicObject; + \\pub const AssignedRoleDetailsMetadata = OpenApi2ZigDynamicObject; + \\pub const MCPListToolsToolAnnotations = OpenApi2ZigDynamicObject; + \\ + \\pub const MCPToolHeaders = std.json.ArrayHashMap([]const u8); + \\ + \\pub const ChatCompletionResponseMessageAudio = struct { + \\ id: []const u8, + \\ expires_at: i64, + \\ data: []const u8, + \\ transcript: []const u8, + \\}; + \\ + \\pub const ChatkitWorkflowStateVariable = union(enum) { + \\ string: []const u8, + \\ integer: i64, + \\ boolean: bool, + \\ number: f64, + \\ + \\ pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { + \\ const value = try std.json.innerParse(std.json.Value, allocator, source, options); + \\ return jsonParseFromValue(allocator, value, options); + \\ } + \\ + \\ pub fn jsonParseFromValue(_: std.mem.Allocator, source: std.json.Value, _: std.json.ParseOptions) !@This() { + \\ return switch (source) { + \\ .string => |value| .{ .string = value }, + \\ .integer => |value| .{ .integer = value }, + \\ .bool => |value| .{ .boolean = value }, + \\ .float => |value| .{ .number = value }, + \\ else => error.UnexpectedToken, + \\ }; + \\ } + \\ + \\ pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void { + \\ switch (self) { + \\ .string => |value| try jw.write(value), + \\ .integer => |value| try jw.write(value), + \\ .boolean => |value| try jw.write(value), + \\ .number => |value| try jw.write(value), + \\ } + \\ } + \\}; + \\ + \\pub const ChatkitWorkflowStateVariables = std.json.ArrayHashMap(ChatkitWorkflowStateVariable); + \\ + \\ + ); + } + + fn generateManualAliases(self: *UnifiedModelGenerator, schemas: std.StringHashMap(Schema)) !void { + if (schemas.contains("ChatkitWorkflow") or schemas.contains("MCPTool") or schemas.contains("ChatCompletionResponseMessage")) { + try self.generateOpenAiDynamicFieldTypes(); + } + if (schemas.contains("InputContent") and !schemas.contains("InputMessageContent")) { + _ = try self.generateManualSchema("InputMessageContent", .{}); + } + if (schemas.contains("CreateChatCompletionStreamResponse") and !schemas.contains("ChatCompletionChunk")) { + try self.buffer.appendSlice(self.allocator, "pub const ChatCompletionChunk = CreateChatCompletionStreamResponse;\n\n"); + } + } + + fn arrayChildSchema(schema: Schema) ?Schema { + if (schema.type == .array or schema.items != null) { + if (schema.items) |items| return items.*; + return null; + } + if (nonNullUnionChild(schema)) |child| return arrayChildSchema(child); + return null; + } + + fn canGenerateNamedArrayItemType(self: *UnifiedModelGenerator, schema: Schema) !bool { + if (schema.ref != null) return false; + if (unionVariants(schema)) |variants| { + if (schema.discriminator_property) |discriminator_property| { + return try self.discriminatorVariantsAreSafe(variants, discriminator_property); + } + return try self.structuralUnionVariantsAreSafe(variants); + } + if (schema.properties) |properties| return properties.count() > 0; + return false; + } + + fn canGenerateNamedFieldType(self: *UnifiedModelGenerator, schema: Schema) !bool { + if (schema.ref != null) return false; + if (arrayChildSchema(schema) != null) return try self.canGenerateNamedArrayItemType(arrayChildSchema(schema).?); + if (unionVariants(schema)) |variants| { + var non_null_count: usize = 0; + for (variants) |variant| { + if (!isNullSchema(variant)) non_null_count += 1; + } + if (non_null_count == 1) { + for (variants) |variant| { + if (isNullSchema(variant)) continue; + return try self.canGenerateNamedFieldType(variant); + } + } + if (schema.discriminator_property) |discriminator_property| { + return try self.discriminatorVariantsAreSafe(variants, discriminator_property); + } + return try self.structuralUnionVariantsAreSafe(variants); + } + if (schema.properties) |properties| return properties.count() > 0; + return false; + } + + fn generateFieldHelpers(self: *UnifiedModelGenerator, owner_name: []const u8, properties: std.StringHashMap(Schema)) !void { + var prop_iterator = properties.iterator(); + while (prop_iterator.next()) |entry| { + const field_name = entry.key_ptr.*; + const field_schema = entry.value_ptr.*; + if (arrayChildSchema(field_schema)) |item_schema| { + if (!try self.canGenerateNamedArrayItemType(item_schema)) continue; + const type_name = try self.arrayFieldItemTypeNameAlloc(owner_name, field_name); + defer self.allocator.free(type_name); + try self.generateSchema(type_name, item_schema); + } else if (try self.canGenerateNamedFieldType(field_schema)) { + const type_name = try self.fieldTypeNameAlloc(owner_name, field_name); + defer self.allocator.free(type_name); + try self.generateSchema(type_name, field_schema); + } + } + } + + fn appendNamedArrayTypeForField(self: *UnifiedModelGenerator, owner_name: []const u8, field_name: []const u8, field_schema: Schema) !bool { + const item_schema = arrayChildSchema(field_schema) orelse return false; + if (!try self.canGenerateNamedArrayItemType(item_schema)) return false; + if (isNullableSchema(field_schema)) try self.buffer.appendSlice(self.allocator, "?"); + const type_name = try self.arrayFieldItemTypeNameAlloc(owner_name, field_name); + defer self.allocator.free(type_name); + try self.buffer.appendSlice(self.allocator, "[]const "); + try self.appendIdentifier(type_name); + return true; + } + + fn appendNamedFieldTypeForField(self: *UnifiedModelGenerator, owner_name: []const u8, field_name: []const u8, field_schema: Schema) !bool { + if (arrayChildSchema(field_schema) != null) return false; + if (!try self.canGenerateNamedFieldType(field_schema)) return false; + if (isNullableSchema(field_schema)) try self.buffer.appendSlice(self.allocator, "?"); + const type_name = try self.fieldTypeNameAlloc(owner_name, field_name); + defer self.allocator.free(type_name); + try self.appendIdentifier(type_name); + return true; + } + + fn generateStructFields(self: *UnifiedModelGenerator, owner_name: []const u8, properties: std.StringHashMap(Schema), required: ?[][]const u8) !void { var prop_iterator = properties.iterator(); while (prop_iterator.next()) |entry| { const field_name = entry.key_ptr.*; const field_schema = entry.value_ptr.*; const is_required = self.isFieldRequired(field_name, required); - try self.generateStructField(field_name, field_schema, is_required); + try self.generateStructField(owner_name, field_name, field_schema, is_required); } } - fn generateStructField(self: *UnifiedModelGenerator, field_name: []const u8, field_schema: Schema, is_required: bool) !void { + fn appendManualFieldType(self: *UnifiedModelGenerator, owner_name: []const u8, field_name: []const u8) !bool { + if (std.mem.eql(u8, owner_name, "ChatkitWorkflow") and std.mem.eql(u8, field_name, "state_variables")) { + try self.buffer.appendSlice(self.allocator, "?ChatkitWorkflowStateVariables"); + return true; + } + if (std.mem.eql(u8, owner_name, "EvalResponsesSource") and std.mem.eql(u8, field_name, "metadata")) { + try self.buffer.appendSlice(self.allocator, "?EvalResponsesSourceMetadata"); + return true; + } + if (std.mem.eql(u8, owner_name, "EvalRunOutputItemResult") and std.mem.eql(u8, field_name, "sample")) { + try self.buffer.appendSlice(self.allocator, "?EvalRunOutputItemResultSample"); + return true; + } + if (std.mem.eql(u8, owner_name, "AssignedRoleDetails") and std.mem.eql(u8, field_name, "created_by_user_obj")) { + try self.buffer.appendSlice(self.allocator, "?AssignedRoleDetailsCreatedByUserObj"); + return true; + } + if (std.mem.eql(u8, owner_name, "AssignedRoleDetails") and std.mem.eql(u8, field_name, "metadata")) { + try self.buffer.appendSlice(self.allocator, "?AssignedRoleDetailsMetadata"); + return true; + } + if (std.mem.eql(u8, owner_name, "MCPListToolsTool") and std.mem.eql(u8, field_name, "annotations")) { + try self.buffer.appendSlice(self.allocator, "?MCPListToolsToolAnnotations"); + return true; + } + if (std.mem.eql(u8, owner_name, "MCPTool") and std.mem.eql(u8, field_name, "headers")) { + try self.buffer.appendSlice(self.allocator, "?MCPToolHeaders"); + return true; + } + if (std.mem.eql(u8, owner_name, "FunctionTool") and std.mem.eql(u8, field_name, "parameters")) { + try self.buffer.appendSlice(self.allocator, "?FunctionParameters"); + return true; + } + if ((std.mem.eql(u8, owner_name, "RunObject") or + std.mem.eql(u8, owner_name, "CreateRunRequest") or + std.mem.eql(u8, owner_name, "CreateThreadAndRunRequest")) and + std.mem.eql(u8, field_name, "tool_choice")) + { + try self.buffer.appendSlice(self.allocator, "?AssistantsApiToolChoiceOption"); + return true; + } + return false; + } + + fn generateStructField(self: *UnifiedModelGenerator, owner_name: []const u8, field_name: []const u8, field_schema: Schema, is_required: bool) !void { try self.buffer.appendSlice(self.allocator, " "); - try self.buffer.appendSlice(self.allocator, field_name); + try self.appendIdentifier(field_name); try self.buffer.appendSlice(self.allocator, ": "); - if (!is_required) { + if (try self.appendManualFieldType(owner_name, field_name)) { + if (!is_required) try self.buffer.appendSlice(self.allocator, " = null"); + try self.buffer.appendSlice(self.allocator, ",\n"); + return; + } + + if (!is_required and !isNullableSchema(field_schema)) { try self.buffer.appendSlice(self.allocator, "?"); } - try self.buffer.appendSlice(self.allocator, self.getZigType(field_schema)); + if (std.mem.eql(u8, field_name, "model")) { + if (is_required and isNullableSchema(field_schema)) try self.buffer.appendSlice(self.allocator, "?"); + if (!is_required and isNullableSchema(field_schema)) try self.buffer.appendSlice(self.allocator, "?"); + try self.buffer.appendSlice(self.allocator, "[]const u8"); + } else if (!try self.appendNamedArrayTypeForField(owner_name, field_name, field_schema)) { + if (!try self.appendNamedFieldTypeForField(owner_name, field_name, field_schema)) { + try self.appendZigType(field_schema); + } + } if (!is_required) { try self.buffer.appendSlice(self.allocator, " = null"); @@ -87,45 +1435,142 @@ pub const UnifiedModelGenerator = struct { try self.buffer.appendSlice(self.allocator, ",\n"); } - fn getZigType(self: *UnifiedModelGenerator, schema: Schema) []const u8 { + fn generateJsonStringify(self: *UnifiedModelGenerator, properties: std.StringHashMap(Schema), required: ?[][]const u8) !void { + try self.buffer.appendSlice(self.allocator, + \\ + \\ pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void { + \\ try jw.beginObject(); + \\ + ); + + var prop_iterator = properties.iterator(); + while (prop_iterator.next()) |entry| { + const field_name = entry.key_ptr.*; + if (self.isFieldRequired(field_name, required)) { + try self.buffer.appendSlice(self.allocator, " try jw.objectField(\""); + try self.buffer.appendSlice(self.allocator, field_name); + try self.buffer.appendSlice(self.allocator, "\");\n"); + try self.buffer.appendSlice(self.allocator, " try jw.write(self."); + try self.appendIdentifier(field_name); + try self.buffer.appendSlice(self.allocator, ");\n"); + } else { + try self.buffer.appendSlice(self.allocator, " if (self."); + try self.appendIdentifier(field_name); + try self.buffer.appendSlice(self.allocator, ") |value| {\n"); + try self.buffer.appendSlice(self.allocator, " try jw.objectField(\""); + try self.buffer.appendSlice(self.allocator, field_name); + try self.buffer.appendSlice(self.allocator, "\");\n"); + try self.buffer.appendSlice(self.allocator, " try jw.write(value);\n"); + try self.buffer.appendSlice(self.allocator, " }\n"); + } + } + + try self.buffer.appendSlice(self.allocator, + \\ + \\ if (self.extra_body) |extra| { + \\ if (extra == .object) { + \\ var iterator = extra.object.iterator(); + \\ while (iterator.next()) |entry| { + \\ try jw.objectField(entry.key_ptr.*); + \\ try jw.write(entry.value_ptr.*); + \\ } + \\ } + \\ } + \\ + \\ try jw.endObject(); + \\ } + \\ + ); + } + + fn appendZigType(self: *UnifiedModelGenerator, schema: Schema) !void { + if (schema.discriminator_property == null and self.isStringLikeSchema(schema)) { + if (isNullableSchema(schema)) try self.buffer.appendSlice(self.allocator, "?"); + try self.buffer.appendSlice(self.allocator, "[]const u8"); + return; + } + + if (schema.discriminator_property == null) { + if (nonNullUnionChild(schema)) |child| { + const variants = unionVariants(schema).?; + var null_count: usize = 0; + for (variants) |variant| { + if (isNullSchema(variant)) null_count += 1; + } + if (null_count == 1 and variants.len == 2) { + try self.buffer.appendSlice(self.allocator, "?"); + try self.appendZigType(child); + return; + } + } + } + + if (schema.items != null and schema.type == null) { + try self.buffer.appendSlice(self.allocator, "[]const "); + try self.appendArrayItemType(schema.items.?.*); + return; + } + + if (schema.ref) |ref| { + if (std.mem.lastIndexOf(u8, ref, "/")) |last_slash| { + try self.appendIdentifier(ref[last_slash + 1 ..]); + return; + } + try self.buffer.appendSlice(self.allocator, "[]const u8"); + return; + } + + if (schema.type) |schema_type| { + switch (schema_type) { + .string => try self.buffer.appendSlice(self.allocator, "[]const u8"), + .integer => try self.buffer.appendSlice(self.allocator, "i64"), + .number => try self.buffer.appendSlice(self.allocator, "f64"), + .boolean => try self.buffer.appendSlice(self.allocator, "bool"), + .array => { + if (schema.items) |items| { + try self.buffer.appendSlice(self.allocator, "[]const "); + try self.appendArrayItemType(items.*); + } else { + try self.buffer.appendSlice(self.allocator, "[]const std.json.Value"); + } + }, + .object, .reference => try self.buffer.appendSlice(self.allocator, "std.json.Value"), + .null => try self.buffer.appendSlice(self.allocator, "void"), + } + return; + } + + try self.buffer.appendSlice(self.allocator, "std.json.Value"); + } + + fn appendArrayItemType(self: *UnifiedModelGenerator, schema: Schema) !void { if (schema.ref) |ref| { if (std.mem.lastIndexOf(u8, ref, "/")) |last_slash| { - const schema_name = ref[last_slash + 1 ..]; - return schema_name; + try self.appendIdentifier(ref[last_slash + 1 ..]); + return; } - return "[]const u8"; } if (schema.type) |schema_type| { - return switch (schema_type) { - .string => "[]const u8", - .integer => "i64", - .number => "f64", - .boolean => "bool", - .array => blk: { + switch (schema_type) { + .string => try self.buffer.appendSlice(self.allocator, "[]const u8"), + .integer => try self.buffer.appendSlice(self.allocator, "i64"), + .number => try self.buffer.appendSlice(self.allocator, "f64"), + .boolean => try self.buffer.appendSlice(self.allocator, "bool"), + .array => { if (schema.items) |items| { - const item_type = self.getZigType(items.*); - if (std.mem.eql(u8, item_type, "[]const u8")) { - break :blk "[]const []const u8"; - } else if (std.mem.eql(u8, item_type, "i64")) { - break :blk "[]const i64"; - } else if (std.mem.eql(u8, item_type, "f64")) { - break :blk "[]const f64"; - } else if (std.mem.eql(u8, item_type, "bool")) { - break :blk "[]const bool"; - } else { - break :blk "[]const std.json.Value"; - } + try self.buffer.appendSlice(self.allocator, "[]const "); + try self.appendArrayItemType(items.*); } else { - break :blk "[]const u8"; + try self.buffer.appendSlice(self.allocator, "std.json.Value"); } }, - .object => "std.json.Value", - .reference => "[]const u8", - }; + else => try self.buffer.appendSlice(self.allocator, "std.json.Value"), + } + return; } - return "[]const u8"; + try self.buffer.appendSlice(self.allocator, "std.json.Value"); } fn isFieldRequired(self: *UnifiedModelGenerator, field_name: []const u8, required: ?[][]const u8) bool { diff --git a/src/models/common/document.zig b/src/models/common/document.zig index f5e1419..fe663f0 100644 --- a/src/models/common/document.zig +++ b/src/models/common/document.zig @@ -69,6 +69,7 @@ pub const SchemaType = enum { array, object, reference, + null, }; pub const Schema = struct { @@ -80,9 +81,14 @@ pub const Schema = struct { required: ?[][]const u8 = null, properties: ?std.StringHashMap(Schema) = null, items: ?*Schema = null, - enum_values: ?[]json.Value = null, + enum_values: ?[]const json.Value = null, default: ?json.Value = null, example: ?json.Value = null, + one_of_refs: ?[][]const u8 = null, + any_of_refs: ?[][]const u8 = null, + discriminator_property: ?[]const u8 = null, + one_of: ?[]Schema = null, + any_of: ?[]Schema = null, pub fn deinit(self: *Schema, allocator: std.mem.Allocator) void { if (self.required) |required| { allocator.free(required); @@ -99,6 +105,23 @@ pub const Schema = struct { items.deinit(allocator); allocator.destroy(items); } + if (self.one_of_refs) |refs| { + for (refs) |ref| allocator.free(ref); + allocator.free(refs); + } + if (self.any_of_refs) |refs| { + for (refs) |ref| allocator.free(ref); + allocator.free(refs); + } + if (self.discriminator_property) |property| allocator.free(property); + if (self.one_of) |variants| { + for (variants) |*variant| variant.deinit(allocator); + allocator.free(variants); + } + if (self.any_of) |variants| { + for (variants) |*variant| variant.deinit(allocator); + allocator.free(variants); + } } }; diff --git a/src/models/v3.1/schema.zig b/src/models/v3.1/schema.zig index e5c101f..543550b 100644 --- a/src/models/v3.1/schema.zig +++ b/src/models/v3.1/schema.zig @@ -3,6 +3,49 @@ const json = std.json; const Reference = @import("reference.zig").Reference; const ExternalDocumentation = @import("externaldocs.zig").ExternalDocumentation; +fn optionalFloat(value: ?json.Value) ?f64 { + const val = value orelse return null; + return switch (val) { + .integer => |i| @as(f64, @floatFromInt(i)), + .float => |f| f, + .number_string => |s| std.fmt.parseFloat(f64, s) catch null, + else => null, + }; +} + +fn optionalInteger(value: ?json.Value) ?i64 { + const val = value orelse return null; + return switch (val) { + .integer => |i| i, + .number_string => |s| std.fmt.parseInt(i64, s, 10) catch null, + else => null, + }; +} + +fn optionalBool(value: ?json.Value) ?bool { + const val = value orelse return null; + return switch (val) { + .bool => |b| b, + else => null, + }; +} + +fn cloneJsonValue(allocator: std.mem.Allocator, value: json.Value) !json.Value { + return switch (value) { + .string => |s| .{ .string = try allocator.dupe(u8, s) }, + .number_string => |s| .{ .number_string = try allocator.dupe(u8, s) }, + else => value, + }; +} + +fn deinitJsonValue(allocator: std.mem.Allocator, value: json.Value) void { + switch (value) { + .string => |s| allocator.free(s), + .number_string => |s| allocator.free(s), + else => {}, + } +} + pub const XML = struct { name: ?[]const u8 = null, namespace: ?[]const u8 = null, @@ -152,7 +195,7 @@ pub const Schema = struct { errdefer enum_list.deinit(allocator); if (obj.get("enum")) |enum_val| { for (enum_val.array.items) |item| { - try enum_list.append(allocator, item); + try enum_list.append(allocator, try cloneJsonValue(allocator, item)); } } var all_of_list = std.ArrayList(SchemaOrReference).empty; @@ -206,19 +249,20 @@ pub const Schema = struct { return Schema{ .title = if (obj.get("title")) |val| try allocator.dupe(u8, val.string) else null, - .multipleOf = if (obj.get("multipleOf")) |val| val.float else null, - .maximum = if (obj.get("maximum")) |val| val.float else null, - .exclusiveMaximum = if (obj.get("exclusiveMaximum")) |val| val.bool else null, - .minimum = if (obj.get("minimum")) |val| val.float else null, - .exclusiveMinimum = if (obj.get("exclusiveMinimum")) |val| val.bool else null, - .maxLength = if (obj.get("maxLength")) |val| val.integer else null, - .minLength = if (obj.get("minLength")) |val| val.integer else null, + .multipleOf = optionalFloat(obj.get("multipleOf")), + .maximum = optionalFloat(obj.get("maximum")), + .exclusiveMaximum = optionalBool(obj.get("exclusiveMaximum")), + .minimum = optionalFloat(obj.get("minimum")), + .exclusiveMinimum = optionalBool(obj.get("exclusiveMinimum")), + .maxLength = optionalInteger(obj.get("maxLength")), + .minLength = optionalInteger(obj.get("minLength")), .pattern = if (obj.get("pattern")) |val| try allocator.dupe(u8, val.string) else null, - .maxItems = if (obj.get("maxItems")) |val| val.integer else null, - .minItems = if (obj.get("minItems")) |val| val.integer else null, - .uniqueItems = if (obj.get("uniqueItems")) |val| val.bool else null, - .maxProperties = if (obj.get("maxProperties")) |val| val.integer else null, - .minProperties = if (obj.get("minProperties")) |val| val.integer else null, + .maxItems = optionalInteger(obj.get("maxItems")), + .minItems = optionalInteger(obj.get("minItems")), + .uniqueItems = optionalBool(obj.get("uniqueItems")), + .maxProperties = optionalInteger(obj.get("maxProperties")), + .minProperties = optionalInteger(obj.get("minProperties")), + .required = if (required_list.items.len > 0) try required_list.toOwnedSlice(allocator) else null, .enum_values = if (enum_list.items.len > 0) try enum_list.toOwnedSlice(allocator) else null, .type = type_str, @@ -311,6 +355,7 @@ pub const Schema = struct { externalDocs.deinit(allocator); } if (self.enum_values) |enum_values| { + for (enum_values) |value| deinitJsonValue(allocator, value); allocator.free(enum_values); } if (self.xml) |*xml| { diff --git a/src/tests.zig b/src/tests.zig index bc063c6..a22b944 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -5,6 +5,8 @@ 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 resource_wrapper_tests = @import("tests/resource_wrapper_tests.zig"); +const model_typing_tests = @import("tests/model_typing_tests.zig"); comptime { _ = openapi_v3_tests; _ = openapi_v31_tests; @@ -13,4 +15,6 @@ comptime { _ = unified_converter_tests; _ = comprehensive_converter_tests; _ = test_input_loader; + _ = resource_wrapper_tests; + _ = model_typing_tests; } diff --git a/src/tests/model_typing_tests.zig b/src/tests/model_typing_tests.zig new file mode 100644 index 0000000..085b8b7 --- /dev/null +++ b/src/tests/model_typing_tests.zig @@ -0,0 +1,323 @@ +const std = @import("std"); +const common = @import("../models/common/document.zig"); +const models = @import("../models.zig"); +const OpenApi31Converter = @import("../generators/converters/openapi31_converter.zig").OpenApi31Converter; +const UnifiedModelGenerator = @import("../generators/unified/model_generator.zig").UnifiedModelGenerator; + +fn stringSchema() common.Schema { + return .{ .type = .string }; +} + +test "model generator treats properties without type as struct" { + const allocator = std.testing.allocator; + var properties = std.StringHashMap(common.Schema).init(allocator); + defer { + var iterator = properties.iterator(); + while (iterator.next()) |entry| allocator.free(entry.key_ptr.*); + properties.deinit(); + } + try properties.put(try allocator.dupe(u8, "foo"), stringSchema()); + + var schemas = std.StringHashMap(common.Schema).init(allocator); + defer { + var iterator = schemas.iterator(); + while (iterator.next()) |entry| allocator.free(entry.key_ptr.*); + schemas.deinit(); + } + try schemas.put(try allocator.dupe(u8, "Foo"), .{ .properties = properties }); + + var paths = std.StringHashMap(common.PathItem).init(allocator); + defer paths.deinit(); + const document: common.UnifiedDocument = .{ + .version = "3.1.0", + .info = .{ .title = "fixture", .version = "1.0.0" }, + .paths = paths, + .schemas = schemas, + }; + + var generator = UnifiedModelGenerator.init(allocator); + defer generator.deinit(); + const code = try generator.generate(document); + defer allocator.free(code); + + try std.testing.expect(std.mem.indexOf(u8, code, "pub const Foo = struct") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "foo: ?[]const u8 = null") != null); +} + +test "OpenAPI 3.1 allOf object refs merge into one schema" { + const allocator = std.testing.allocator; + const source = + \\{ + \\ "openapi": "3.1.0", + \\ "info": { "title": "fixture", "version": "1.0.0" }, + \\ "paths": {}, + \\ "components": { + \\ "schemas": { + \\ "Base": { + \\ "type": "object", + \\ "required": ["id"], + \\ "properties": { "id": { "type": "string" } } + \\ }, + \\ "Thing": { + \\ "allOf": [ + \\ { "$ref": "#/components/schemas/Base" }, + \\ { + \\ "type": "object", + \\ "required": ["name"], + \\ "properties": { "name": { "type": "string" } } + \\ } + \\ ] + \\ } + \\ } + \\ } + \\} + ; + + var parsed = try models.OpenApi31Document.parseFromJson(allocator, source); + defer parsed.deinit(allocator); + + var converter = OpenApi31Converter.init(allocator); + var unified = try converter.convert(parsed); + defer unified.deinit(allocator); + + const schemas = unified.schemas.?; + const thing = schemas.get("Thing").?; + try std.testing.expect(thing.properties != null); + try std.testing.expect(thing.properties.?.contains("id")); + try std.testing.expect(thing.properties.?.contains("name")); + try std.testing.expect(thing.required != null); + try std.testing.expectEqual(@as(usize, 2), thing.required.?.len); +} + +test "OpenAPI 3.1 discriminator oneOf emits tagged union with raw fallback" { + const allocator = std.testing.allocator; + const source = + \\{ + \\ "openapi": "3.1.0", + \\ "info": { "title": "fixture", "version": "1.0.0" }, + \\ "paths": {}, + \\ "components": { + \\ "schemas": { + \\ "Cat": { + \\ "type": "object", + \\ "required": ["type", "meows"], + \\ "properties": { + \\ "type": { "type": "string", "enum": ["cat"] }, + \\ "meows": { "type": "boolean" } + \\ } + \\ }, + \\ "Dog": { + \\ "type": "object", + \\ "required": ["type", "barks"], + \\ "properties": { + \\ "type": { "type": "string", "enum": ["dog"] }, + \\ "barks": { "type": "boolean" } + \\ } + \\ }, + \\ "Pet": { + \\ "oneOf": [ + \\ { "$ref": "#/components/schemas/Cat" }, + \\ { "$ref": "#/components/schemas/Dog" } + \\ ], + \\ "discriminator": { "propertyName": "type" } + \\ } + \\ } + \\ } + \\} + ; + + var parsed = try models.OpenApi31Document.parseFromJson(allocator, source); + defer parsed.deinit(allocator); + + var converter = OpenApi31Converter.init(allocator); + var unified = try converter.convert(parsed); + defer unified.deinit(allocator); + + var generator = UnifiedModelGenerator.init(allocator); + defer generator.deinit(); + const code = try generator.generate(unified); + defer allocator.free(code); + + try std.testing.expect(std.mem.indexOf(u8, code, "pub const Pet = union(enum)") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "cat: Cat") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "dog: Dog") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "raw: std.json.Value") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub fn jsonParseFromValue") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "source.object.get(\"type\")") != null); +} + +test "extensible request structs emit extra_body merge hook" { + const allocator = std.testing.allocator; + var properties = std.StringHashMap(common.Schema).init(allocator); + defer { + var iterator = properties.iterator(); + while (iterator.next()) |entry| allocator.free(entry.key_ptr.*); + properties.deinit(); + } + try properties.put(try allocator.dupe(u8, "model"), stringSchema()); + + var schemas = std.StringHashMap(common.Schema).init(allocator); + defer { + var iterator = schemas.iterator(); + while (iterator.next()) |entry| allocator.free(entry.key_ptr.*); + schemas.deinit(); + } + try schemas.put(try allocator.dupe(u8, "CreateChatCompletionRequest"), .{ + .type = .object, + .properties = properties, + .required = try allocator.dupe([]const u8, &.{"model"}), + }); + defer allocator.free(schemas.getPtr("CreateChatCompletionRequest").?.required.?); + + var paths = std.StringHashMap(common.PathItem).init(allocator); + defer paths.deinit(); + const document: common.UnifiedDocument = .{ + .version = "3.1.0", + .info = .{ .title = "fixture", .version = "1.0.0" }, + .paths = paths, + .schemas = schemas, + }; + + var generator = UnifiedModelGenerator.init(allocator); + defer generator.deinit(); + const code = try generator.generate(document); + defer allocator.free(code); + + try std.testing.expect(std.mem.indexOf(u8, code, "extra_body: ?std.json.Value = null") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "var iterator = extra.object.iterator()") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "try jw.objectField(entry.key_ptr.*)") != null); +} + +test "model generator emits named types for structured array items" { + const allocator = std.testing.allocator; + const source = + \\{ + \\ "openapi": "3.1.0", + \\ "info": { "title": "fixture", "version": "1.0.0" }, + \\ "paths": {}, + \\ "components": { + \\ "schemas": { + \\ "Cat": { + \\ "type": "object", + \\ "required": ["type", "meows"], + \\ "properties": { + \\ "type": { "type": "string", "enum": ["cat"] }, + \\ "meows": { "type": "boolean" } + \\ } + \\ }, + \\ "Dog": { + \\ "type": "object", + \\ "required": ["type", "barks"], + \\ "properties": { + \\ "type": { "type": "string", "enum": ["dog"] }, + \\ "barks": { "type": "boolean" } + \\ } + \\ }, + \\ "Owner": { + \\ "type": "object", + \\ "required": ["pets", "rows"], + \\ "properties": { + \\ "pets": { + \\ "type": "array", + \\ "items": { + \\ "oneOf": [ + \\ { "$ref": "#/components/schemas/Cat" }, + \\ { "$ref": "#/components/schemas/Dog" } + \\ ], + \\ "discriminator": { "propertyName": "type" } + \\ } + \\ }, + \\ "rows": { + \\ "type": "array", + \\ "items": { + \\ "type": "object", + \\ "required": ["name"], + \\ "properties": { "name": { "type": "string" } } + \\ } + \\ } + \\ } + \\ } + \\ } + \\ } + \\} + ; + + var parsed = try models.OpenApi31Document.parseFromJson(allocator, source); + defer parsed.deinit(allocator); + + var converter = OpenApi31Converter.init(allocator); + var unified = try converter.convert(parsed); + defer unified.deinit(allocator); + + var generator = UnifiedModelGenerator.init(allocator); + defer generator.deinit(); + const code = try generator.generate(unified); + defer allocator.free(code); + + try std.testing.expect(std.mem.indexOf(u8, code, "pub const OwnerPetsItem = union(enum)") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "cat: Cat") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "dog: Dog") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pets: []const OwnerPetsItem") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub const OwnerRowsItem = struct") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "rows: []const OwnerRowsItem") != null); +} + +test "model generator emits named types for field-level composite schemas" { + const allocator = std.testing.allocator; + const source = + \\{ + \\ "openapi": "3.1.0", + \\ "info": { "title": "fixture", "version": "1.0.0" }, + \\ "paths": {}, + \\ "components": { + \\ "schemas": { + \\ "TextPart": { + \\ "type": "object", + \\ "required": ["type", "text"], + \\ "properties": { + \\ "type": { "type": "string", "enum": ["text"] }, + \\ "text": { "type": "string" } + \\ } + \\ }, + \\ "Thing": { + \\ "type": "object", + \\ "required": ["value", "content"], + \\ "properties": { + \\ "value": { + \\ "oneOf": [ + \\ { "type": "string" }, + \\ { "type": "integer" } + \\ ] + \\ }, + \\ "content": { + \\ "oneOf": [ + \\ { "type": "string" }, + \\ { "type": "array", "items": { "$ref": "#/components/schemas/TextPart" } } + \\ ] + \\ } + \\ } + \\ } + \\ } + \\ } + \\} + ; + + var parsed = try models.OpenApi31Document.parseFromJson(allocator, source); + defer parsed.deinit(allocator); + + var converter = OpenApi31Converter.init(allocator); + var unified = try converter.convert(parsed); + defer unified.deinit(allocator); + + var generator = UnifiedModelGenerator.init(allocator); + defer generator.deinit(); + const code = try generator.generate(unified); + defer allocator.free(code); + + try std.testing.expect(std.mem.indexOf(u8, code, "pub const ThingValue = union(enum)") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "value: ThingValue") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub const ThingContent = union(enum)") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "text_part_items: []const TextPart") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "content: ThingContent") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "[]const std.json.Value") == null); +} diff --git a/src/tests/resource_wrapper_tests.zig b/src/tests/resource_wrapper_tests.zig new file mode 100644 index 0000000..84fb8c3 --- /dev/null +++ b/src/tests/resource_wrapper_tests.zig @@ -0,0 +1,107 @@ +const std = @import("std"); +const cli = @import("../cli.zig"); +const UnifiedApiGenerator = @import("../generators/unified/api_generator.zig").UnifiedApiGenerator; +const common = @import("../models/common/document.zig"); + +fn responseMap(allocator: std.mem.Allocator, with_schema: bool) !std.StringHashMap(common.Response) { + var responses = std.StringHashMap(common.Response).init(allocator); + errdefer responses.deinit(); + try responses.put(try allocator.dupe(u8, if (with_schema) "200" else "204"), .{ + .description = "ok", + .schema = if (with_schema) common.Schema{ .type = .object } else null, + }); + return responses; +} + +fn op(allocator: std.mem.Allocator, operation_id: []const u8, method: []const u8, has_body: bool, has_path_param: bool, has_response: bool) !common.Operation { + var params = std.ArrayList(common.Parameter).empty; + errdefer params.deinit(allocator); + + if (has_path_param) { + try params.append(allocator, .{ + .name = "petId", + .location = .path, + .required = true, + .type = .integer, + }); + } + if (has_body) { + try params.append(allocator, .{ + .name = "body", + .location = .body, + .required = true, + .schema = .{ .type = .object }, + }); + } + + _ = method; + return .{ + .operationId = operation_id, + .parameters = if (params.items.len == 0) null else try params.toOwnedSlice(allocator), + .responses = try responseMap(allocator, has_response), + }; +} + +fn buildFixture(allocator: std.mem.Allocator) !common.UnifiedDocument { + var paths = std.StringHashMap(common.PathItem).init(allocator); + errdefer paths.deinit(); + + try paths.put(try allocator.dupe(u8, "/pets"), .{ + .get = try op(allocator, "listPets", "GET", false, false, true), + .post = try op(allocator, "createPet", "POST", true, false, true), + }); + try paths.put(try allocator.dupe(u8, "/pets/{petId}"), .{ + .get = try op(allocator, "getPet", "GET", false, true, true), + .delete = try op(allocator, "deletePet", "DELETE", false, true, false), + }); + try paths.put(try allocator.dupe(u8, "/chat/completions"), .{ + .post = try op(allocator, "createChatCompletion", "POST", true, false, true), + }); + + return .{ + .version = "3.0.0", + .info = .{ .title = "fixture", .version = "1.0.0" }, + .paths = paths, + }; +} + +test "resource wrappers derive from paths" { + const allocator = std.testing.allocator; + var document = try buildFixture(allocator); + defer document.deinit(allocator); + + var generator = UnifiedApiGenerator.init(allocator, .{ + .input_path = "fixture.json", + .resource_wrappers = .paths, + }); + defer generator.deinit(); + + const code = try generator.generate(document); + defer allocator.free(code); + + try std.testing.expect(std.mem.indexOf(u8, code, "pub const resources = struct") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub const pets = struct") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub fn list(client: *Client)") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "return listPetsResult(client);") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub fn listPetsRaw(client: *Client) !RawResponse") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub fn listResult(client: *Client) !ApiResult(std.json.Value)") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "return listPetsResult(client);") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub fn create(client: *Client, requestBody: std.json.Value)") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "return createPet(client, requestBody);") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub fn createPetRaw(client: *Client, requestBody: std.json.Value) !RawResponse") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub fn createResult(client: *Client, requestBody: std.json.Value) !ApiResult(std.json.Value)") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "return createPetResult(client, requestBody);") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub fn get(client: *Client, petId: i64)") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "return getPet(client, petId);") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub fn getPetRaw(client: *Client, petId: i64) !RawResponse") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub fn getPetResult(client: *Client, petId: i64) !ApiResult(std.json.Value)") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub fn getResult(client: *Client, petId: i64) !ApiResult(std.json.Value)") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub fn delete(client: *Client, petId: i64)") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "return deletePet(client, petId);") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub const chat = struct") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub const completions = struct") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "return createChatCompletion(client, requestBody);") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "return createChatCompletionResult(client, requestBody);") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub const chat = resources.chat;") != null); + try std.testing.expect(std.mem.indexOf(u8, code, "pub const pets = resources.pets;") != null); +}