From fe5e9ea11a4c2ecf464cef032eb3af2e46391fe7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:03:35 +0000 Subject: [PATCH 1/7] Initial plan From 22f9c4e7c4d0acc20dbe14e207333e3201f0168e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:12:13 +0000 Subject: [PATCH 2/7] Add JsonServiceClient implementation with examples and documentation Co-authored-by: mythz <89361+mythz@users.noreply.github.com> --- .github/workflows/ci.yml | 30 +++++ LICENSE | 28 +++++ README.md | 122 ++++++++++++++++++- USAGE.md | 250 +++++++++++++++++++++++++++++++++++++++ build.zig | 45 +++++++ build.zig.zon | 16 +++ examples/advanced.zig | 179 ++++++++++++++++++++++++++++ examples/basic.zig | 44 +++++++ src/client.zig | 176 +++++++++++++++++++++++++++ 9 files changed, 889 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 LICENSE create mode 100644 USAGE.md create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 examples/advanced.zig create mode 100644 examples/basic.zig create mode 100644 src/client.zig diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ef77ce5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup Zig + uses: goto-bus-stop/setup-zig@v2 + with: + version: 0.11.0 + + - name: Build + run: zig build + + - name: Run tests + run: zig build test + + - name: Build examples + run: | + zig build-exe examples/basic.zig -femit-bin=zig-out/bin/basic + zig build-exe examples/advanced.zig -femit-bin=zig-out/bin/advanced diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f231fd7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2024, ServiceStack + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 14b521a..4da46e8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,122 @@ # servicestack-zig -ServiceStack Client Zig Library + +ServiceStack Client Zig Library - A JsonServiceClient for making API requests to ServiceStack services. + +## Overview + +This library provides a `JsonServiceClient` for the Zig programming language that enables you to easily consume ServiceStack APIs using strongly-typed DTOs (Data Transfer Objects). + +## Features + +- ✅ HTTP GET, POST, PUT, DELETE, and PATCH support +- ✅ JSON serialization/deserialization +- ✅ Strongly-typed request/response DTOs +- ✅ Configurable timeouts +- ✅ Simple and intuitive API + +## Installation + +Add this package to your `build.zig.zon` dependencies: + +```zig +.dependencies = .{ + .servicestack = .{ + .url = "https://github.com/ServiceStack/servicestack-zig/archive/refs/heads/main.tar.gz", + }, +}, +``` + +## Usage + +### Basic Example + +```zig +const std = @import("std"); +const servicestack = @import("servicestack"); + +// Define your ServiceStack DTOs +const Hello = struct { + name: []const u8, +}; + +const HelloResponse = struct { + result: []const u8, +}; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + // Initialize the JsonServiceClient + var client = try servicestack.JsonServiceClient.init( + allocator, + "https://test.servicestack.net" + ); + defer client.deinit(); + + // Create a request DTO + const request = Hello{ .name = "World" }; + + // Make a POST request + const response = try client.post(HelloResponse, "/hello", request); + std.debug.print("Result: {s}\n", .{response.result}); +} +``` + +### HTTP Methods + +The `JsonServiceClient` supports all common HTTP methods: + +```zig +// GET request +const response = try client.get(MyResponse, "/api/resource"); + +// POST request +const response = try client.post(MyResponse, "/api/resource", request); + +// PUT request +const response = try client.put(MyResponse, "/api/resource", request); + +// DELETE request +const response = try client.delete(MyResponse, "/api/resource"); + +// PATCH request +const response = try client.patch(MyResponse, "/api/resource", request); +``` + +### Configuration + +```zig +// Set custom timeout (in milliseconds) +client.setTimeout(60000); // 60 seconds +``` + +## Building + +```bash +# Build the library +zig build + +# Run tests +zig build test + +# Run the example +zig build example +``` + +## Adding ServiceStack Reference + +To use this client with your ServiceStack services, generate Zig DTOs using [Add ServiceStack Reference](https://docs.servicestack.net/add-servicestack-reference): + +1. Use the ServiceStack `x` tool or your ServiceStack instance to generate DTOs +2. Add the generated Zig DTO files to your project +3. Import and use them with the `JsonServiceClient` + +## Requirements + +- Zig 0.11.0 or later + +## License + +See LICENSE file for details. diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..218773d --- /dev/null +++ b/USAGE.md @@ -0,0 +1,250 @@ +# ServiceStack Zig Client Usage Guide + +## Introduction + +The ServiceStack Zig client library provides a `JsonServiceClient` class for making HTTP requests to ServiceStack services. This guide demonstrates how to use the client with ServiceStack DTOs. + +## Quick Start + +### 1. Import the Library + +```zig +const std = @import("std"); +const servicestack = @import("servicestack"); +``` + +### 2. Define Your DTOs + +ServiceStack DTOs are simple Zig structs that match your service's request/response contracts: + +```zig +// Request DTO +const Hello = struct { + name: []const u8, +}; + +// Response DTO +const HelloResponse = struct { + result: []const u8, +}; +``` + +### 3. Initialize the Client + +```zig +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + var client = try servicestack.JsonServiceClient.init( + allocator, + "https://your-api.com" + ); + defer client.deinit(); +} +``` + +### 4. Make API Requests + +```zig +// POST request with DTO +const request = Hello{ .name = "World" }; +const response = try client.post(HelloResponse, "/hello", request); + +// GET request +const users = try client.get(UsersResponse, "/users"); + +// PUT request +const updated = try client.put(UpdateResponse, "/users/1", updateRequest); + +// DELETE request +const deleted = try client.delete(DeleteResponse, "/users/1"); +``` + +## Advanced Usage + +### Custom Timeout + +Set a custom timeout for requests (in milliseconds): + +```zig +client.setTimeout(60000); // 60 seconds +``` + +### Complex DTOs + +ServiceStack DTOs can include nested structures, arrays, and optional fields: + +```zig +const User = struct { + id: i32, + name: []const u8, + email: []const u8, + roles: [][]const u8, + metadata: ?Metadata, +}; + +const Metadata = struct { + created_at: []const u8, + updated_at: []const u8, +}; + +const CreateUserRequest = struct { + user: User, +}; + +const CreateUserResponse = struct { + id: i32, + success: bool, + message: []const u8, +}; +``` + +### Error Handling + +The client returns Zig errors for various failure conditions: + +```zig +const response = client.post(HelloResponse, "/hello", request) catch |err| { + switch (err) { + error.HttpError => { + std.debug.print("HTTP request failed\n", .{}); + }, + error.OutOfMemory => { + std.debug.print("Out of memory\n", .{}); + }, + else => { + std.debug.print("Unknown error: {}\n", .{err}); + }, + } + return err; +}; +``` + +## ServiceStack DTO Generation + +### Using Add ServiceStack Reference + +ServiceStack supports "Add ServiceStack Reference" for generating strongly-typed DTOs in various languages. While Zig support may need to be added to the ServiceStack ecosystem, you can: + +1. **Manually create DTOs**: Define Zig structs that match your service's contracts +2. **Convert from other languages**: If you have DTOs in other languages, convert them to Zig structs +3. **Use JSON examples**: Create structs based on actual JSON responses from your API + +### DTO Naming Conventions + +Follow Zig naming conventions when creating DTOs: + +```zig +// PascalCase for struct names +const HelloRequest = struct { + // snake_case for field names (or match your API's convention) + name: []const u8, + greeting_type: []const u8, +}; +``` + +## Complete Example + +```zig +const std = @import("std"); +const servicestack = @import("servicestack"); + +// DTOs for a todo service +const Todo = struct { + id: i32, + title: []const u8, + completed: bool, +}; + +const GetTodosResponse = struct { + todos: []Todo, + total: i32, +}; + +const CreateTodoRequest = struct { + title: []const u8, +}; + +const CreateTodoResponse = struct { + todo: Todo, + success: bool, +}; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + // Initialize client + var client = try servicestack.JsonServiceClient.init( + allocator, + "https://api.example.com" + ); + defer client.deinit(); + + // Set timeout + client.setTimeout(30000); + + // Get all todos + const todos_response = try client.get(GetTodosResponse, "/todos"); + std.debug.print("Found {} todos\n", .{todos_response.total}); + + // Create a new todo + const create_request = CreateTodoRequest{ + .title = "Learn Zig with ServiceStack" + }; + const create_response = try client.post( + CreateTodoResponse, + "/todos", + create_request + ); + + if (create_response.success) { + std.debug.print("Created todo #{}: {s}\n", .{ + create_response.todo.id, + create_response.todo.title, + }); + } +} +``` + +## Best Practices + +1. **Always defer deinit**: Call `defer client.deinit()` right after initialization +2. **Use allocators properly**: Pass the same allocator to the client that you use for your application +3. **Handle errors**: Always handle potential errors from API calls +4. **Define clear DTOs**: Keep your DTO structures simple and match your API contracts exactly +5. **Set appropriate timeouts**: Configure timeouts based on your API's expected response times + +## Tips + +- Use `std.testing.allocator` in tests for memory leak detection +- DTOs with `[]const u8` fields for strings will reference the JSON response buffer +- For long-lived response data, make copies with `allocator.dupe()` +- The client automatically sets `Content-Type` and `Accept` headers to `application/json` + +## Troubleshooting + +### Memory Issues + +If you encounter memory issues: +- Ensure you're calling `client.deinit()` +- Check that response data doesn't outlive the client +- Use `std.testing.allocator` to detect leaks in tests + +### JSON Parsing Errors + +If JSON parsing fails: +- Verify your DTO structure matches the API response +- Use `ignore_unknown_fields = true` (already set by default) +- Check that field types match (i32 vs i64, etc.) + +### Network Errors + +If requests fail: +- Verify the base URL is correct +- Check that the endpoint path is valid +- Ensure the server is accessible +- Increase timeout for slow connections diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..3c87ba7 --- /dev/null +++ b/build.zig @@ -0,0 +1,45 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // Create the module + const servicestack_module = b.addModule("servicestack", .{ + .source_file = .{ .path = "src/client.zig" }, + }); + + // Create a library + const lib = b.addStaticLibrary(.{ + .name = "servicestack-zig", + .root_source_file = .{ .path = "src/client.zig" }, + .target = target, + .optimize = optimize, + }); + b.installArtifact(lib); + + // Create tests + const tests = b.addTest(.{ + .root_source_file = .{ .path = "src/client.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_tests = b.addRunArtifact(tests); + const test_step = b.step("test", "Run library tests"); + test_step.dependOn(&run_tests.step); + + // Create example + const example = b.addExecutable(.{ + .name = "example", + .root_source_file = .{ .path = "examples/basic.zig" }, + .target = target, + .optimize = optimize, + }); + example.addModule("servicestack", servicestack_module); + b.installArtifact(example); + + const run_example = b.addRunArtifact(example); + const example_step = b.step("example", "Run example"); + example_step.dependOn(&run_example.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..e5701e9 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,16 @@ +.{ + .name = "servicestack-zig", + .version = "0.1.0", + .minimum_zig_version = "0.11.0", + + .dependencies = .{}, + + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + "examples", + "README.md", + "LICENSE", + }, +} diff --git a/examples/advanced.zig b/examples/advanced.zig new file mode 100644 index 0000000..9fa64a1 --- /dev/null +++ b/examples/advanced.zig @@ -0,0 +1,179 @@ +const std = @import("std"); +const servicestack = @import("servicestack"); + +// Example DTOs for a more complex ServiceStack API +// These would typically be generated via "Add ServiceStack Reference" + +// User-related DTOs +const User = struct { + id: i32, + username: []const u8, + email: []const u8, + created_at: []const u8, +}; + +const AuthenticateRequest = struct { + username: []const u8, + password: []const u8, + provider: []const u8 = "credentials", +}; + +const AuthenticateResponse = struct { + user_id: i32, + session_id: []const u8, + username: []const u8, + bearer_token: []const u8, +}; + +// Todo-related DTOs +const Todo = struct { + id: i32, + user_id: i32, + title: []const u8, + description: ?[]const u8, + completed: bool, + priority: i32, +}; + +const CreateTodoRequest = struct { + title: []const u8, + description: ?[]const u8, + priority: i32 = 1, +}; + +const CreateTodoResponse = struct { + todo: Todo, + success: bool, + error_message: ?[]const u8, +}; + +const GetTodosRequest = struct { + user_id: i32, + completed: ?bool, + limit: i32 = 10, + offset: i32 = 0, +}; + +const GetTodosResponse = struct { + todos: []Todo, + total: i32, + has_more: bool, +}; + +const UpdateTodoRequest = struct { + id: i32, + title: ?[]const u8, + description: ?[]const u8, + completed: ?bool, + priority: ?i32, +}; + +const UpdateTodoResponse = struct { + todo: Todo, + success: bool, +}; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + std.debug.print("=== ServiceStack Zig Client - Advanced Example ===\n\n", .{}); + + // Initialize the client + var client = try servicestack.JsonServiceClient.init( + allocator, + "https://api.example.com", + ); + defer client.deinit(); + + // Configure timeout + client.setTimeout(30000); // 30 seconds + + std.debug.print("Client initialized successfully!\n", .{}); + std.debug.print("Base URL: {s}\n", .{client.base_url}); + std.debug.print("Timeout: {}ms\n\n", .{client.timeout_ms}); + + // Example 1: Authentication + std.debug.print("Example 1: Authentication\n", .{}); + std.debug.print("-------------------------\n", .{}); + const auth_request = AuthenticateRequest{ + .username = "user@example.com", + .password = "password123", + }; + std.debug.print("Request: POST /auth/login\n", .{}); + std.debug.print(" Username: {s}\n", .{auth_request.username}); + std.debug.print(" Provider: {s}\n\n", .{auth_request.provider}); + // In real usage: + // const auth_response = try client.post(AuthenticateResponse, "/auth/login", auth_request); + // std.debug.print("Authenticated! Session: {s}\n\n", .{auth_response.session_id}); + + // Example 2: Create a Todo + std.debug.print("Example 2: Create a Todo\n", .{}); + std.debug.print("------------------------\n", .{}); + const create_request = CreateTodoRequest{ + .title = "Learn Zig programming", + .description = "Complete the ServiceStack Zig client integration", + .priority = 1, + }; + std.debug.print("Request: POST /todos\n", .{}); + std.debug.print(" Title: {s}\n", .{create_request.title}); + if (create_request.description) |desc| { + std.debug.print(" Description: {s}\n", .{desc}); + } + std.debug.print(" Priority: {}\n\n", .{create_request.priority}); + // In real usage: + // const create_response = try client.post(CreateTodoResponse, "/todos", create_request); + + // Example 3: Get Todos with filters + std.debug.print("Example 3: Get Todos with Filters\n", .{}); + std.debug.print("---------------------------------\n", .{}); + const get_request = GetTodosRequest{ + .user_id = 1, + .completed = false, + .limit = 20, + .offset = 0, + }; + std.debug.print("Request: POST /todos/query\n", .{}); + std.debug.print(" User ID: {}\n", .{get_request.user_id}); + if (get_request.completed) |comp| { + std.debug.print(" Completed: {}\n", .{comp}); + } + std.debug.print(" Limit: {}\n", .{get_request.limit}); + std.debug.print(" Offset: {}\n\n", .{get_request.offset}); + // In real usage: + // const todos_response = try client.post(GetTodosResponse, "/todos/query", get_request); + + // Example 4: Update a Todo + std.debug.print("Example 4: Update a Todo\n", .{}); + std.debug.print("------------------------\n", .{}); + const update_request = UpdateTodoRequest{ + .id = 1, + .title = null, + .description = null, + .completed = true, + .priority = null, + }; + std.debug.print("Request: PUT /todos/1\n", .{}); + std.debug.print(" ID: {}\n", .{update_request.id}); + if (update_request.completed) |comp| { + std.debug.print(" Completed: {}\n", .{comp}); + } + std.debug.print("\n", .{}); + // In real usage: + // const update_response = try client.put(UpdateTodoResponse, "/todos/1", update_request); + + // Example 5: Delete a Todo + std.debug.print("Example 5: Delete a Todo\n", .{}); + std.debug.print("------------------------\n", .{}); + std.debug.print("Request: DELETE /todos/1\n", .{}); + // In real usage: + // _ = try client.delete(DeleteResponse, "/todos/1"); + + std.debug.print("\n=== All examples completed successfully! ===\n", .{}); + std.debug.print("\nTo use with a real ServiceStack API:\n", .{}); + std.debug.print("1. Replace the base URL with your API endpoint\n", .{}); + std.debug.print("2. Define DTOs matching your service contracts\n", .{}); + std.debug.print("3. Uncomment the actual API calls\n", .{}); + std.debug.print("4. Handle responses appropriately\n", .{}); +} diff --git a/examples/basic.zig b/examples/basic.zig new file mode 100644 index 0000000..fc662f7 --- /dev/null +++ b/examples/basic.zig @@ -0,0 +1,44 @@ +const std = @import("std"); +const servicestack = @import("servicestack"); + +// Define your ServiceStack DTOs +const Hello = struct { + name: []const u8, +}; + +const HelloResponse = struct { + result: []const u8, +}; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + // Initialize the JsonServiceClient with your ServiceStack API base URL + var client = try servicestack.JsonServiceClient.init( + allocator, + "https://test.servicestack.net", + ); + defer client.deinit(); + + // Create a request DTO + const request = Hello{ .name = "Zig" }; + + std.debug.print("Making request to ServiceStack API...\n", .{}); + std.debug.print("Request: Hello {{ name: \"{s}\" }}\n", .{request.name}); + + // Make a POST request + // Note: This is an example and will fail without a real endpoint + // In a real scenario, you would use: + // const response = try client.post(HelloResponse, "/hello", request); + // std.debug.print("Response: {s}\n", .{response.result}); + + std.debug.print("\nJsonServiceClient initialized successfully!\n", .{}); + std.debug.print("Base URL: {s}\n", .{client.base_url}); + std.debug.print("Timeout: {}ms\n", .{client.timeout_ms}); + + std.debug.print("\nExample of how to use the client:\n", .{}); + std.debug.print(" const response = try client.post(HelloResponse, \"/hello\", request);\n", .{}); + std.debug.print(" std.debug.print(\"Result: {{s}}\\n\", .{{response.result}});\n", .{}); +} diff --git a/src/client.zig b/src/client.zig new file mode 100644 index 0000000..04286a5 --- /dev/null +++ b/src/client.zig @@ -0,0 +1,176 @@ +const std = @import("std"); +const http = std.http; +const json = std.json; +const mem = std.mem; + +/// JsonServiceClient is a client for making HTTP requests to ServiceStack services +/// It handles JSON serialization/deserialization and provides a simple API +pub const JsonServiceClient = struct { + allocator: mem.Allocator, + base_url: []const u8, + timeout_ms: u32, + + const Self = @This(); + + /// Initialize a new JsonServiceClient + pub fn init(allocator: mem.Allocator, base_url: []const u8) !Self { + return Self{ + .allocator = allocator, + .base_url = try allocator.dupe(u8, base_url), + .timeout_ms = 30000, // 30 second default timeout + }; + } + + /// Deinitialize and cleanup resources + pub fn deinit(self: *Self) void { + self.allocator.free(self.base_url); + } + + /// Set the timeout for requests in milliseconds + pub fn setTimeout(self: *Self, timeout_ms: u32) void { + self.timeout_ms = timeout_ms; + } + + /// Send a GET request and parse the response + pub fn get(self: *Self, comptime TResponse: type, path: []const u8) !TResponse { + return self.send(TResponse, .GET, path, null); + } + + /// Send a POST request with a request DTO and parse the response + pub fn post(self: *Self, comptime TResponse: type, path: []const u8, request: anytype) !TResponse { + return self.send(TResponse, .POST, path, request); + } + + /// Send a PUT request with a request DTO and parse the response + pub fn put(self: *Self, comptime TResponse: type, path: []const u8, request: anytype) !TResponse { + return self.send(TResponse, .PUT, path, request); + } + + /// Send a DELETE request and parse the response + pub fn delete(self: *Self, comptime TResponse: type, path: []const u8) !TResponse { + return self.send(TResponse, .DELETE, path, null); + } + + /// Send a PATCH request with a request DTO and parse the response + pub fn patch(self: *Self, comptime TResponse: type, path: []const u8, request: anytype) !TResponse { + return self.send(TResponse, .PATCH, path, request); + } + + /// Internal method to send HTTP requests + fn send( + self: *Self, + comptime TResponse: type, + method: http.Method, + path: []const u8, + request: anytype, + ) !TResponse { + var client = http.Client{ .allocator = self.allocator }; + defer client.deinit(); + + // Build full URL + const url = try std.fmt.allocPrint(self.allocator, "{s}{s}", .{ self.base_url, path }); + defer self.allocator.free(url); + + const uri = try std.Uri.parse(url); + + // Prepare request body if provided + var body_buffer: ?[]const u8 = null; + defer if (body_buffer) |buf| self.allocator.free(buf); + + if (request != null) { + var body_list = std.ArrayList(u8).init(self.allocator); + defer body_list.deinit(); + + try json.stringify(request, .{}, body_list.writer()); + body_buffer = try body_list.toOwnedSlice(); + } + + // Create headers + var headers = http.Headers{ .allocator = self.allocator }; + defer headers.deinit(); + + try headers.append("accept", "application/json"); + try headers.append("content-type", "application/json"); + + // Make the request + var req = try client.open(method, uri, .{ + .server_header_buffer = try self.allocator.alloc(u8, 8192), + .headers = headers, + }); + defer req.deinit(); + + req.transfer_encoding = .chunked; + + try req.send(); + + if (body_buffer) |body| { + try req.writeAll(body); + } + + try req.finish(); + try req.wait(); + + // Read response + var response_buffer = std.ArrayList(u8).init(self.allocator); + defer response_buffer.deinit(); + + const max_size = 10 * 1024 * 1024; // 10MB max + try req.reader().readAllArrayList(&response_buffer, max_size); + + // Check status code + if (req.response.status != .ok) { + return error.HttpError; + } + + // Parse JSON response + const parsed = try json.parseFromSlice( + TResponse, + self.allocator, + response_buffer.items, + .{ .ignore_unknown_fields = true }, + ); + + return parsed.value; + } +}; + +/// Example DTO structure for demonstration +pub const HelloResponse = struct { + result: []const u8, +}; + +pub const Hello = struct { + name: []const u8, +}; + +// Tests +test "JsonServiceClient init and deinit" { + const allocator = std.testing.allocator; + var client = try JsonServiceClient.init(allocator, "https://example.org"); + defer client.deinit(); + + try std.testing.expectEqualStrings("https://example.org", client.base_url); + try std.testing.expectEqual(@as(u32, 30000), client.timeout_ms); +} + +test "JsonServiceClient setTimeout" { + const allocator = std.testing.allocator; + var client = try JsonServiceClient.init(allocator, "https://example.org"); + defer client.deinit(); + + client.setTimeout(5000); + try std.testing.expectEqual(@as(u32, 5000), client.timeout_ms); +} + +test "Hello DTO structure" { + const allocator = std.testing.allocator; + const hello = Hello{ .name = "World" }; + + var list = std.ArrayList(u8).init(allocator); + defer list.deinit(); + + try json.stringify(hello, .{}, list.writer()); + const result = list.items; + + try std.testing.expect(mem.indexOf(u8, result, "World") != null); +} From bb073cb3b1938115656f2486e15a1b6dc3a3908c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:14:59 +0000 Subject: [PATCH 3/7] Add comprehensive documentation and DTO examples Co-authored-by: mythz <89361+mythz@users.noreply.github.com> --- ADD_SERVICESTACK_REFERENCE.md | 277 ++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 218 ++++++++++++++++++++++++++ examples/dtos.zig | 286 ++++++++++++++++++++++++++++++++++ 3 files changed, 781 insertions(+) create mode 100644 ADD_SERVICESTACK_REFERENCE.md create mode 100644 CONTRIBUTING.md create mode 100644 examples/dtos.zig diff --git a/ADD_SERVICESTACK_REFERENCE.md b/ADD_SERVICESTACK_REFERENCE.md new file mode 100644 index 0000000..21b2936 --- /dev/null +++ b/ADD_SERVICESTACK_REFERENCE.md @@ -0,0 +1,277 @@ +# Add ServiceStack Reference for Zig + +This document describes how to use ServiceStack DTOs with the Zig JsonServiceClient. + +## Overview + +[Add ServiceStack Reference](https://docs.servicestack.net/add-servicestack-reference) is ServiceStack's feature for generating strongly-typed client DTOs from your service contracts. While native Zig DTO generation support may be added to ServiceStack in the future, you can currently create Zig DTOs manually based on your service contracts. + +## Creating Zig DTOs + +### Manual DTO Creation + +Based on your ServiceStack service's metadata, create corresponding Zig structs: + +#### Example ServiceStack C# DTOs: + +```csharp +[Route("/hello")] +public class Hello : IReturn +{ + public string Name { get; set; } +} + +public class HelloResponse +{ + public string Result { get; set; } +} +``` + +#### Corresponding Zig DTOs: + +```zig +const Hello = struct { + name: []const u8, +}; + +const HelloResponse = struct { + result: []const u8, +}; +``` + +### Type Mappings + +Map ServiceStack/C# types to Zig types: + +| ServiceStack/C# | Zig Type | +|-----------------|----------| +| `string` | `[]const u8` | +| `int` | `i32` | +| `long` | `i64` | +| `bool` | `bool` | +| `double` | `f64` | +| `float` | `f32` | +| `DateTime` | `[]const u8` (ISO 8601 string) | +| `List` | `[]T` | +| `T?` (nullable) | `?T` | + +### Field Naming Conventions + +ServiceStack typically uses PascalCase for property names, but JSON serialization often uses camelCase. The Zig client uses the field names as-is, so match your API's JSON format: + +```zig +// If your API returns: { "firstName": "John", "lastName": "Doe" } +const User = struct { + firstName: []const u8, + lastName: []const u8, +}; + +// If your API returns: { "first_name": "John", "last_name": "Doe" } +const User = struct { + first_name: []const u8, + last_name: []const u8, +}; +``` + +## Complete Example + +### 1. ServiceStack Service (C#) + +```csharp +[Route("/todos", "GET")] +public class GetTodos : IReturn +{ + public int UserId { get; set; } + public bool? Completed { get; set; } +} + +public class GetTodosResponse +{ + public List Todos { get; set; } + public int Total { get; set; } +} + +public class Todo +{ + public int Id { get; set; } + public string Title { get; set; } + public bool Completed { get; set; } +} +``` + +### 2. Zig DTOs + +```zig +const GetTodos = struct { + userId: i32, + completed: ?bool, +}; + +const GetTodosResponse = struct { + todos: []Todo, + total: i32, +}; + +const Todo = struct { + id: i32, + title: []const u8, + completed: bool, +}; +``` + +### 3. Usage + +```zig +const std = @import("std"); +const servicestack = @import("servicestack"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + var client = try servicestack.JsonServiceClient.init( + allocator, + "https://your-api.com" + ); + defer client.deinit(); + + const request = GetTodos{ + .userId = 1, + .completed = null, + }; + + const response = try client.post(GetTodosResponse, "/todos", request); + + std.debug.print("Found {} todos\n", .{response.total}); + for (response.todos) |todo| { + std.debug.print("- {s} (completed: {})\n", .{todo.title, todo.completed}); + } +} +``` + +## Working with Complex Types + +### Nested Objects + +```zig +const User = struct { + id: i32, + name: []const u8, + profile: Profile, +}; + +const Profile = struct { + bio: []const u8, + avatar_url: []const u8, +}; +``` + +### Optional Fields + +Use Zig's optional type `?T` for nullable fields: + +```zig +const Todo = struct { + id: i32, + title: []const u8, + description: ?[]const u8, // Can be null + due_date: ?[]const u8, // Can be null +}; +``` + +### Arrays/Lists + +```zig +const User = struct { + id: i32, + name: []const u8, + roles: [][]const u8, // Array of strings + permissions: []Permission, // Array of objects +}; +``` + +### Enums + +Map ServiceStack enums to Zig enums: + +```csharp +public enum Priority +{ + Low = 0, + Medium = 1, + High = 2 +} +``` + +```zig +const Priority = enum(i32) { + Low = 0, + Medium = 1, + High = 2, +}; + +const Todo = struct { + id: i32, + title: []const u8, + priority: Priority, +}; +``` + +## Best Practices + +1. **Organize DTOs by Service**: Group related DTOs in separate Zig files +2. **Use Consistent Naming**: Match field names to your API's JSON format exactly +3. **Document Special Cases**: Add comments for fields with special handling +4. **Version DTOs**: Keep DTOs in sync with your ServiceStack service version +5. **Share Common Types**: Extract shared types (like `ResponseStatus`) to a common file + +## Example Project Structure + +``` +your-project/ +├── build.zig +├── src/ +│ ├── main.zig +│ └── dtos/ +│ ├── common.zig # Shared types +│ ├── auth.zig # Authentication DTOs +│ ├── todos.zig # Todo service DTOs +│ └── users.zig # User service DTOs +└── examples/ + └── api_usage.zig +``` + +### Common DTOs (common.zig): + +```zig +pub const ResponseStatus = struct { + error_code: ?[]const u8, + message: ?[]const u8, + stack_trace: ?[]const u8, + errors: ?[]ResponseError, +}; + +pub const ResponseError = struct { + error_code: []const u8, + field_name: []const u8, + message: []const u8, +}; +``` + +## Future Integration + +ServiceStack may add native Zig support to "Add ServiceStack Reference" in the future, which would allow automatic generation of Zig DTOs from your service metadata. Until then, manually creating DTOs as shown in this guide is the recommended approach. + +## Tips + +1. Use your ServiceStack service's `/types/zig` endpoint (once supported) or `/types/typescript` as a reference +2. Test your DTOs with sample JSON responses to ensure correct structure +3. Use `std.json.stringify` to validate request DTO serialization +4. Use `std.json.parseFromSlice` to test response parsing manually if needed + +## Resources + +- [ServiceStack Add Reference](https://docs.servicestack.net/add-servicestack-reference) +- [Zig JSON Documentation](https://ziglang.org/documentation/master/std/#std;json) +- [ServiceStack Client Architecture](https://docs.servicestack.net/clients-overview) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..45ba015 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,218 @@ +# Contributing to ServiceStack Zig + +Thank you for your interest in contributing to the ServiceStack Zig client library! + +## Development Setup + +### Prerequisites + +- Zig 0.11.0 or later +- Git + +### Getting Started + +1. Fork the repository +2. Clone your fork: + ```bash + git clone https://github.com/YOUR_USERNAME/servicestack-zig.git + cd servicestack-zig + ``` + +3. Build the project: + ```bash + zig build + ``` + +4. Run tests: + ```bash + zig build test + ``` + +## Project Structure + +``` +servicestack-zig/ +├── src/ +│ └── client.zig # Main JsonServiceClient implementation +├── examples/ +│ ├── basic.zig # Basic usage example +│ └── advanced.zig # Advanced usage with multiple DTOs +├── build.zig # Build configuration +├── build.zig.zon # Package metadata +├── README.md # Main documentation +├── USAGE.md # Detailed usage guide +├── ADD_SERVICESTACK_REFERENCE.md # DTO creation guide +└── .github/ + └── workflows/ + └── ci.yml # CI configuration +``` + +## Making Changes + +### Code Style + +Follow the Zig standard library conventions: +- 4 spaces for indentation +- Snake_case for function names +- PascalCase for type names +- Descriptive variable names +- Clear documentation comments + +### Testing + +All new features and bug fixes should include tests: + +```zig +test "descriptive test name" { + const allocator = std.testing.allocator; + + // Your test code here + + try std.testing.expectEqual(expected, actual); +} +``` + +Run tests with: +```bash +zig build test +``` + +### Documentation + +- Add doc comments to all public APIs +- Update README.md if adding new features +- Add examples for new functionality +- Keep USAGE.md up to date + +### Commit Messages + +Use clear, descriptive commit messages: +- Use present tense ("Add feature" not "Added feature") +- First line should be 50 chars or less +- Reference issues and pull requests when relevant + +Example: +``` +Add support for custom headers + +- Add setHeader method to JsonServiceClient +- Update documentation with header examples +- Add tests for header functionality + +Fixes #123 +``` + +## Submitting Changes + +1. Create a feature branch: + ```bash + git checkout -b feature/your-feature-name + ``` + +2. Make your changes and commit: + ```bash + git add . + git commit -m "Your descriptive commit message" + ``` + +3. Push to your fork: + ```bash + git push origin feature/your-feature-name + ``` + +4. Open a Pull Request + +### Pull Request Guidelines + +- Provide a clear description of the changes +- Reference any related issues +- Ensure all tests pass +- Update documentation as needed +- Keep changes focused and minimal + +## Areas for Contribution + +### High Priority + +- [ ] Custom header support +- [ ] Authentication integration (Bearer tokens, API keys) +- [ ] Better error handling and custom error types +- [ ] Response status handling +- [ ] Request/response interceptors +- [ ] Streaming support for large responses + +### Medium Priority + +- [ ] Connection pooling +- [ ] Retry logic with exponential backoff +- [ ] Request caching +- [ ] Multipart form data support +- [ ] File upload/download helpers +- [ ] WebSocket support + +### Documentation + +- [ ] More comprehensive examples +- [ ] Integration guides for popular ServiceStack services +- [ ] Tutorial videos or blog posts +- [ ] API reference documentation + +### Infrastructure + +- [ ] Benchmark suite +- [ ] Integration tests with real ServiceStack service +- [ ] Performance profiling +- [ ] Memory usage optimization + +## Testing with Real ServiceStack Services + +When testing with real ServiceStack services: + +1. Use the public test instance at `https://test.servicestack.net` +2. Or set up a local ServiceStack service for testing +3. Add integration tests in a separate directory +4. Document any setup required + +## Code Review Process + +1. All submissions require review from maintainers +2. Address review feedback promptly +3. Keep discussions focused and professional +4. Be open to suggestions and improvements + +## Bug Reports + +When reporting bugs, please include: + +- Zig version +- Operating system +- Minimal reproduction code +- Expected vs actual behavior +- Error messages or stack traces + +Use the GitHub issue template when available. + +## Feature Requests + +For feature requests: + +- Describe the use case clearly +- Explain why the feature would be useful +- Provide examples if possible +- Discuss potential implementation approaches + +## Questions and Support + +- GitHub Issues: For bug reports and feature requests +- GitHub Discussions: For questions and community discussion +- ServiceStack Forums: For ServiceStack-specific questions + +## License + +By contributing, you agree that your contributions will be licensed under the same license as the project (BSD 3-Clause). + +## Recognition + +Contributors will be recognized in the README and release notes. + +Thank you for contributing to ServiceStack Zig! diff --git a/examples/dtos.zig b/examples/dtos.zig new file mode 100644 index 0000000..c5765c9 --- /dev/null +++ b/examples/dtos.zig @@ -0,0 +1,286 @@ +// Example DTOs that would typically be generated via "Add ServiceStack Reference" +// This file demonstrates how to structure DTOs for use with the JsonServiceClient + +const std = @import("std"); + +// ============================================================================ +// Common Response Types +// ============================================================================ + +/// Standard ServiceStack ResponseStatus for error handling +pub const ResponseStatus = struct { + error_code: ?[]const u8 = null, + message: ?[]const u8 = null, + stack_trace: ?[]const u8 = null, + errors: ?[]ResponseError = null, +}; + +pub const ResponseError = struct { + error_code: []const u8, + field_name: []const u8, + message: []const u8, +}; + +// ============================================================================ +// Authentication DTOs +// ============================================================================ + +/// Request DTO for authenticating with ServiceStack +pub const Authenticate = struct { + provider: []const u8 = "credentials", + username: ?[]const u8 = null, + password: ?[]const u8 = null, + remember_me: ?bool = null, + access_token: ?[]const u8 = null, + access_token_secret: ?[]const u8 = null, +}; + +/// Response DTO from authentication +pub const AuthenticateResponse = struct { + user_id: []const u8, + session_id: []const u8, + username: []const u8, + display_name: ?[]const u8 = null, + bearer_token: []const u8, + refresh_token: ?[]const u8 = null, + profile_url: ?[]const u8 = null, + roles: ?[][]const u8 = null, + permissions: ?[][]const u8 = null, + response_status: ?ResponseStatus = null, +}; + +// ============================================================================ +// User Management DTOs +// ============================================================================ + +pub const User = struct { + id: i32, + username: []const u8, + email: []const u8, + first_name: ?[]const u8 = null, + last_name: ?[]const u8 = null, + display_name: ?[]const u8 = null, + created_date: []const u8, + modified_date: ?[]const u8 = null, +}; + +pub const GetUser = struct { + id: i32, +}; + +pub const GetUserResponse = struct { + user: User, + response_status: ?ResponseStatus = null, +}; + +pub const CreateUser = struct { + username: []const u8, + email: []const u8, + password: []const u8, + first_name: ?[]const u8 = null, + last_name: ?[]const u8 = null, +}; + +pub const CreateUserResponse = struct { + user: User, + response_status: ?ResponseStatus = null, +}; + +pub const UpdateUser = struct { + id: i32, + email: ?[]const u8 = null, + first_name: ?[]const u8 = null, + last_name: ?[]const u8 = null, + display_name: ?[]const u8 = null, +}; + +pub const UpdateUserResponse = struct { + user: User, + response_status: ?ResponseStatus = null, +}; + +pub const DeleteUser = struct { + id: i32, +}; + +pub const DeleteUserResponse = struct { + success: bool, + response_status: ?ResponseStatus = null, +}; + +// ============================================================================ +// Todo Management DTOs +// ============================================================================ + +pub const Priority = enum(i32) { + Low = 0, + Medium = 1, + High = 2, + Critical = 3, +}; + +pub const Todo = struct { + id: i32, + user_id: i32, + title: []const u8, + description: ?[]const u8 = null, + completed: bool = false, + priority: Priority = .Medium, + due_date: ?[]const u8 = null, + created_date: []const u8, + modified_date: ?[]const u8 = null, +}; + +pub const GetTodos = struct { + user_id: ?i32 = null, + completed: ?bool = null, + priority: ?Priority = null, + limit: i32 = 100, + offset: i32 = 0, + order_by: ?[]const u8 = null, +}; + +pub const GetTodosResponse = struct { + todos: []Todo, + total: i32, + offset: i32, + limit: i32, + response_status: ?ResponseStatus = null, +}; + +pub const GetTodo = struct { + id: i32, +}; + +pub const GetTodoResponse = struct { + todo: Todo, + response_status: ?ResponseStatus = null, +}; + +pub const CreateTodo = struct { + title: []const u8, + description: ?[]const u8 = null, + priority: Priority = .Medium, + due_date: ?[]const u8 = null, +}; + +pub const CreateTodoResponse = struct { + todo: Todo, + response_status: ?ResponseStatus = null, +}; + +pub const UpdateTodo = struct { + id: i32, + title: ?[]const u8 = null, + description: ?[]const u8 = null, + completed: ?bool = null, + priority: ?Priority = null, + due_date: ?[]const u8 = null, +}; + +pub const UpdateTodoResponse = struct { + todo: Todo, + response_status: ?ResponseStatus = null, +}; + +pub const DeleteTodo = struct { + id: i32, +}; + +pub const DeleteTodoResponse = struct { + success: bool, + response_status: ?ResponseStatus = null, +}; + +// ============================================================================ +// Query DTOs with Filtering +// ============================================================================ + +pub const QueryResponse = struct { + offset: i32, + total: i32, + results: []Todo, // Generic, would be specific type in real usage + response_status: ?ResponseStatus = null, +}; + +// ============================================================================ +// Example Usage +// ============================================================================ + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + std.debug.print("=== ServiceStack Zig DTOs Example ===\n\n", .{}); + + // Example 1: Authentication DTO + const auth_request = Authenticate{ + .provider = "credentials", + .username = "user@example.com", + .password = "password123", + .remember_me = true, + }; + std.debug.print("Authentication Request:\n", .{}); + std.debug.print(" Provider: {s}\n", .{auth_request.provider}); + if (auth_request.username) |username| { + std.debug.print(" Username: {s}\n", .{username}); + } + if (auth_request.remember_me) |remember| { + std.debug.print(" Remember Me: {}\n\n", .{remember}); + } + + // Example 2: Create Todo DTO + const create_todo = CreateTodo{ + .title = "Learn Zig programming", + .description = "Complete the ServiceStack integration", + .priority = .High, + .due_date = "2024-12-31T23:59:59Z", + }; + std.debug.print("Create Todo Request:\n", .{}); + std.debug.print(" Title: {s}\n", .{create_todo.title}); + if (create_todo.description) |desc| { + std.debug.print(" Description: {s}\n", .{desc}); + } + std.debug.print(" Priority: {}\n", .{create_todo.priority}); + if (create_todo.due_date) |due| { + std.debug.print(" Due Date: {s}\n\n", .{due}); + } + + // Example 3: Query Todos DTO + const query_todos = GetTodos{ + .user_id = 1, + .completed = false, + .priority = .High, + .limit = 20, + .offset = 0, + .order_by = "created_date DESC", + }; + std.debug.print("Query Todos Request:\n", .{}); + if (query_todos.user_id) |user_id| { + std.debug.print(" User ID: {}\n", .{user_id}); + } + if (query_todos.completed) |completed| { + std.debug.print(" Completed: {}\n", .{completed}); + } + if (query_todos.priority) |priority| { + std.debug.print(" Priority: {}\n", .{priority}); + } + std.debug.print(" Limit: {}\n", .{query_todos.limit}); + std.debug.print(" Offset: {}\n", .{query_todos.offset}); + if (query_todos.order_by) |order_by| { + std.debug.print(" Order By: {s}\n\n", .{order_by}); + } + + // Example 4: JSON serialization + var json_buffer = std.ArrayList(u8).init(allocator); + defer json_buffer.deinit(); + + try std.json.stringify(create_todo, .{}, json_buffer.writer()); + std.debug.print("Serialized CreateTodo to JSON:\n{s}\n\n", .{json_buffer.items}); + + std.debug.print("These DTOs would be used with JsonServiceClient like:\n", .{}); + std.debug.print(" const response = try client.post(AuthenticateResponse, \"/auth\", auth_request);\n", .{}); + std.debug.print(" const todo = try client.post(CreateTodoResponse, \"/todos\", create_todo);\n", .{}); + std.debug.print(" const todos = try client.post(GetTodosResponse, \"/todos/query\", query_todos);\n", .{}); +} From 85ba42fb19cf777dc2f3a819a595e3db2ad0842c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:16:31 +0000 Subject: [PATCH 4/7] Add quick start guide for easy onboarding Co-authored-by: mythz <89361+mythz@users.noreply.github.com> --- QUICKSTART.md | 257 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 QUICKSTART.md diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..87215a9 --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,257 @@ +# Quick Start Guide + +Get started with ServiceStack Zig in 5 minutes! + +## 1. Install Zig + +Download and install Zig 0.11.0 or later from [ziglang.org](https://ziglang.org/download/) + +## 2. Create a New Project + +```bash +mkdir my-servicestack-app +cd my-servicestack-app +zig init-exe +``` + +## 3. Add ServiceStack Zig Dependency + +Edit `build.zig.zon` and add: + +```zig +.{ + .name = "my-servicestack-app", + .version = "0.1.0", + .dependencies = .{ + .servicestack = .{ + .url = "https://github.com/ServiceStack/servicestack-zig/archive/refs/heads/main.tar.gz", + }, + }, +} +``` + +## 4. Update build.zig + +Edit `build.zig` to include the servicestack module: + +```zig +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // Get servicestack dependency + const servicestack = b.dependency("servicestack", .{ + .target = target, + .optimize = optimize, + }); + + const exe = b.addExecutable(.{ + .name = "my-servicestack-app", + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + // Add servicestack module to your executable + exe.addModule("servicestack", servicestack.module("servicestack")); + + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); +} +``` + +## 5. Write Your First ServiceStack Client + +Edit `src/main.zig`: + +```zig +const std = @import("std"); +const servicestack = @import("servicestack"); + +// Define your DTOs +const Hello = struct { + name: []const u8, +}; + +const HelloResponse = struct { + result: []const u8, +}; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + // Create the client + var client = try servicestack.JsonServiceClient.init( + allocator, + "https://test.servicestack.net" + ); + defer client.deinit(); + + // Make a request + const request = Hello{ .name = "World" }; + const response = try client.post(HelloResponse, "/hello", request); + + std.debug.print("Result: {s}\n", .{response.result}); +} +``` + +## 6. Build and Run + +```bash +zig build run +``` + +## What's Next? + +### Explore Examples + +Check out the examples directory: +- `examples/basic.zig` - Simple usage +- `examples/advanced.zig` - Complex DTOs and multiple requests +- `examples/dtos.zig` - DTO patterns and best practices + +### Read Documentation + +- [USAGE.md](USAGE.md) - Detailed usage guide +- [ADD_SERVICESTACK_REFERENCE.md](ADD_SERVICESTACK_REFERENCE.md) - Creating DTOs +- [README.md](README.md) - Full documentation + +### Common Tasks + +#### Define DTOs for Your API + +Match your ServiceStack service contracts: + +```zig +const User = struct { + id: i32, + name: []const u8, + email: []const u8, +}; + +const GetUser = struct { + id: i32, +}; + +const GetUserResponse = struct { + user: User, +}; +``` + +#### Make Different HTTP Requests + +```zig +// GET request +const user = try client.get(GetUserResponse, "/users/1"); + +// POST request +const created = try client.post(CreateUserResponse, "/users", create_request); + +// PUT request +const updated = try client.put(UpdateUserResponse, "/users/1", update_request); + +// DELETE request +_ = try client.delete(DeleteResponse, "/users/1"); +``` + +#### Configure Timeout + +```zig +client.setTimeout(60000); // 60 seconds +``` + +#### Handle Errors + +```zig +const response = client.get(MyResponse, "/endpoint") catch |err| { + std.debug.print("Error: {}\n", .{err}); + return err; +}; +``` + +## Troubleshooting + +### Build Errors + +If you get build errors: +1. Ensure you're using Zig 0.11.0 or later: `zig version` +2. Try cleaning the build cache: `rm -rf zig-cache zig-out` +3. Rebuild: `zig build` + +### Runtime Errors + +If requests fail: +1. Check that your base URL is correct +2. Verify your DTOs match the API response structure +3. Check network connectivity +4. Try increasing timeout: `client.setTimeout(60000)` + +### Need Help? + +- Open an issue on [GitHub](https://github.com/ServiceStack/servicestack-zig/issues) +- Check the [ServiceStack Community](https://forums.servicestack.net/) +- Read the detailed [documentation](README.md) + +## Example: Real-World Todo App + +Here's a complete example with CRUD operations: + +```zig +const std = @import("std"); +const servicestack = @import("servicestack"); + +const Todo = struct { + id: i32, + title: []const u8, + completed: bool, +}; + +const CreateTodo = struct { + title: []const u8, +}; + +const CreateTodoResponse = struct { + todo: Todo, +}; + +const GetTodosResponse = struct { + todos: []Todo, +}; + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + var client = try servicestack.JsonServiceClient.init( + allocator, + "https://your-api.com" + ); + defer client.deinit(); + + // Create a todo + const create = CreateTodo{ .title = "Learn Zig" }; + const created = try client.post(CreateTodoResponse, "/todos", create); + std.debug.print("Created todo #{}: {s}\n", .{ created.todo.id, created.todo.title }); + + // Get all todos + const todos = try client.get(GetTodosResponse, "/todos"); + std.debug.print("Found {} todos\n", .{todos.todos.len}); + + for (todos.todos) |todo| { + const status = if (todo.completed) "✓" else " "; + std.debug.print("[{s}] {}: {s}\n", .{ status, todo.id, todo.title }); + } +} +``` + +Happy coding with ServiceStack and Zig! 🚀 From 32e0497c63c58e1d2ec28ed7c135d357d0f49d10 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:27:39 +0000 Subject: [PATCH 5/7] Fix memory management and update API to return Parsed() wrapper Co-authored-by: mythz <89361+mythz@users.noreply.github.com> --- .github/workflows/ci.yml | 6 ++---- ADD_SERVICESTACK_REFERENCE.md | 7 ++++--- QUICKSTART.md | 39 ++++++++++++++++++++++++----------- README.md | 25 +++++++++++++++------- USAGE.md | 34 ++++++++++++++++++++---------- build.zig | 2 +- examples/advanced.zig | 17 +++++++++------ examples/basic.zig | 10 +++++---- examples/dtos.zig | 7 ++++--- src/client.zig | 27 +++++++++++++++--------- 10 files changed, 113 insertions(+), 61 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef77ce5..3b3abaa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,5 @@ jobs: - name: Run tests run: zig build test - - name: Build examples - run: | - zig build-exe examples/basic.zig -femit-bin=zig-out/bin/basic - zig build-exe examples/advanced.zig -femit-bin=zig-out/bin/advanced + - name: Build example + run: zig build example diff --git a/ADD_SERVICESTACK_REFERENCE.md b/ADD_SERVICESTACK_REFERENCE.md index 21b2936..aebd4db 100644 --- a/ADD_SERVICESTACK_REFERENCE.md +++ b/ADD_SERVICESTACK_REFERENCE.md @@ -141,10 +141,11 @@ pub fn main() !void { .completed = null, }; - const response = try client.post(GetTodosResponse, "/todos", request); + const parsed = try client.post(GetTodosResponse, "/todos", request); + defer parsed.deinit(); - std.debug.print("Found {} todos\n", .{response.total}); - for (response.todos) |todo| { + std.debug.print("Found {} todos\n", .{parsed.value.total}); + for (parsed.value.todos) |todo| { std.debug.print("- {s} (completed: {})\n", .{todo.title, todo.completed}); } } diff --git a/QUICKSTART.md b/QUICKSTART.md index 87215a9..ca33e8f 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -98,9 +98,10 @@ pub fn main() !void { // Make a request const request = Hello{ .name = "World" }; - const response = try client.post(HelloResponse, "/hello", request); + const parsed = try client.post(HelloResponse, "/hello", request); + defer parsed.deinit(); - std.debug.print("Result: {s}\n", .{response.result}); + std.debug.print("Result: {s}\n", .{parsed.value.result}); } ``` @@ -151,16 +152,23 @@ const GetUserResponse = struct { ```zig // GET request -const user = try client.get(GetUserResponse, "/users/1"); +const parsed_user = try client.get(GetUserResponse, "/users/1"); +defer parsed_user.deinit(); +const user = parsed_user.value; // POST request -const created = try client.post(CreateUserResponse, "/users", create_request); +const parsed_create = try client.post(CreateUserResponse, "/users", create_request); +defer parsed_create.deinit(); +const created = parsed_create.value; // PUT request -const updated = try client.put(UpdateUserResponse, "/users/1", update_request); +const parsed_update = try client.put(UpdateUserResponse, "/users/1", update_request); +defer parsed_update.deinit(); +const updated = parsed_update.value; // DELETE request -_ = try client.delete(DeleteResponse, "/users/1"); +const parsed_delete = try client.delete(DeleteResponse, "/users/1"); +defer parsed_delete.deinit(); ``` #### Configure Timeout @@ -172,10 +180,12 @@ client.setTimeout(60000); // 60 seconds #### Handle Errors ```zig -const response = client.get(MyResponse, "/endpoint") catch |err| { +const parsed = client.get(MyResponse, "/endpoint") catch |err| { std.debug.print("Error: {}\n", .{err}); return err; }; +defer parsed.deinit(); +const response = parsed.value; ``` ## Troubleshooting @@ -240,14 +250,19 @@ pub fn main() !void { // Create a todo const create = CreateTodo{ .title = "Learn Zig" }; - const created = try client.post(CreateTodoResponse, "/todos", create); - std.debug.print("Created todo #{}: {s}\n", .{ created.todo.id, created.todo.title }); + const parsed_create = try client.post(CreateTodoResponse, "/todos", create); + defer parsed_create.deinit(); + std.debug.print("Created todo #{}: {s}\n", .{ + parsed_create.value.todo.id, + parsed_create.value.todo.title + }); // Get all todos - const todos = try client.get(GetTodosResponse, "/todos"); - std.debug.print("Found {} todos\n", .{todos.todos.len}); + const parsed_todos = try client.get(GetTodosResponse, "/todos"); + defer parsed_todos.deinit(); + std.debug.print("Found {} todos\n", .{parsed_todos.value.todos.len}); - for (todos.todos) |todo| { + for (parsed_todos.value.todos) |todo| { const status = if (todo.completed) "✓" else " "; std.debug.print("[{s}] {}: {s}\n", .{ status, todo.id, todo.title }); } diff --git a/README.md b/README.md index 4da46e8..4f29127 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,9 @@ pub fn main() !void { const request = Hello{ .name = "World" }; // Make a POST request - const response = try client.post(HelloResponse, "/hello", request); - std.debug.print("Result: {s}\n", .{response.result}); + const parsed = try client.post(HelloResponse, "/hello", request); + defer parsed.deinit(); + std.debug.print("Result: {s}\n", .{parsed.value.result}); } ``` @@ -70,19 +71,29 @@ The `JsonServiceClient` supports all common HTTP methods: ```zig // GET request -const response = try client.get(MyResponse, "/api/resource"); +const parsed = try client.get(MyResponse, "/api/resource"); +defer parsed.deinit(); +const response = parsed.value; // POST request -const response = try client.post(MyResponse, "/api/resource", request); +const parsed = try client.post(MyResponse, "/api/resource", request); +defer parsed.deinit(); +const response = parsed.value; // PUT request -const response = try client.put(MyResponse, "/api/resource", request); +const parsed = try client.put(MyResponse, "/api/resource", request); +defer parsed.deinit(); +const response = parsed.value; // DELETE request -const response = try client.delete(MyResponse, "/api/resource"); +const parsed = try client.delete(MyResponse, "/api/resource"); +defer parsed.deinit(); +const response = parsed.value; // PATCH request -const response = try client.patch(MyResponse, "/api/resource", request); +const parsed = try client.patch(MyResponse, "/api/resource", request); +defer parsed.deinit(); +const response = parsed.value; ``` ### Configuration diff --git a/USAGE.md b/USAGE.md index 218773d..5ade820 100644 --- a/USAGE.md +++ b/USAGE.md @@ -50,16 +50,24 @@ pub fn main() !void { ```zig // POST request with DTO const request = Hello{ .name = "World" }; -const response = try client.post(HelloResponse, "/hello", request); +const parsed = try client.post(HelloResponse, "/hello", request); +defer parsed.deinit(); +const response = parsed.value; // GET request -const users = try client.get(UsersResponse, "/users"); +const parsed_users = try client.get(UsersResponse, "/users"); +defer parsed_users.deinit(); +const users = parsed_users.value; // PUT request -const updated = try client.put(UpdateResponse, "/users/1", updateRequest); +const parsed_update = try client.put(UpdateResponse, "/users/1", updateRequest); +defer parsed_update.deinit(); +const updated = parsed_update.value; // DELETE request -const deleted = try client.delete(DeleteResponse, "/users/1"); +const parsed_delete = try client.delete(DeleteResponse, "/users/1"); +defer parsed_delete.deinit(); +const deleted = parsed_delete.value; ``` ## Advanced Usage @@ -106,7 +114,7 @@ const CreateUserResponse = struct { The client returns Zig errors for various failure conditions: ```zig -const response = client.post(HelloResponse, "/hello", request) catch |err| { +const parsed = client.post(HelloResponse, "/hello", request) catch |err| { switch (err) { error.HttpError => { std.debug.print("HTTP request failed\n", .{}); @@ -120,6 +128,8 @@ const response = client.post(HelloResponse, "/hello", request) catch |err| { } return err; }; +defer parsed.deinit(); +const response = parsed.value; ``` ## ServiceStack DTO Generation @@ -188,23 +198,25 @@ pub fn main() !void { client.setTimeout(30000); // Get all todos - const todos_response = try client.get(GetTodosResponse, "/todos"); - std.debug.print("Found {} todos\n", .{todos_response.total}); + const parsed_todos = try client.get(GetTodosResponse, "/todos"); + defer parsed_todos.deinit(); + std.debug.print("Found {} todos\n", .{parsed_todos.value.total}); // Create a new todo const create_request = CreateTodoRequest{ .title = "Learn Zig with ServiceStack" }; - const create_response = try client.post( + const parsed_create = try client.post( CreateTodoResponse, "/todos", create_request ); + defer parsed_create.deinit(); - if (create_response.success) { + if (parsed_create.value.success) { std.debug.print("Created todo #{}: {s}\n", .{ - create_response.todo.id, - create_response.todo.title, + parsed_create.value.todo.id, + parsed_create.value.todo.title, }); } } diff --git a/build.zig b/build.zig index 3c87ba7..1632aae 100644 --- a/build.zig +++ b/build.zig @@ -6,7 +6,7 @@ pub fn build(b: *std.Build) void { // Create the module const servicestack_module = b.addModule("servicestack", .{ - .source_file = .{ .path = "src/client.zig" }, + .root_source_file = .{ .path = "src/client.zig" }, }); // Create a library diff --git a/examples/advanced.zig b/examples/advanced.zig index 9fa64a1..bde6c64 100644 --- a/examples/advanced.zig +++ b/examples/advanced.zig @@ -105,8 +105,9 @@ pub fn main() !void { std.debug.print(" Username: {s}\n", .{auth_request.username}); std.debug.print(" Provider: {s}\n\n", .{auth_request.provider}); // In real usage: - // const auth_response = try client.post(AuthenticateResponse, "/auth/login", auth_request); - // std.debug.print("Authenticated! Session: {s}\n\n", .{auth_response.session_id}); + // const parsed_auth = try client.post(AuthenticateResponse, "/auth/login", auth_request); + // defer parsed_auth.deinit(); + // std.debug.print("Authenticated! Session: {s}\n\n", .{parsed_auth.value.session_id}); // Example 2: Create a Todo std.debug.print("Example 2: Create a Todo\n", .{}); @@ -123,7 +124,8 @@ pub fn main() !void { } std.debug.print(" Priority: {}\n\n", .{create_request.priority}); // In real usage: - // const create_response = try client.post(CreateTodoResponse, "/todos", create_request); + // const parsed_create = try client.post(CreateTodoResponse, "/todos", create_request); + // defer parsed_create.deinit(); // Example 3: Get Todos with filters std.debug.print("Example 3: Get Todos with Filters\n", .{}); @@ -142,7 +144,8 @@ pub fn main() !void { std.debug.print(" Limit: {}\n", .{get_request.limit}); std.debug.print(" Offset: {}\n\n", .{get_request.offset}); // In real usage: - // const todos_response = try client.post(GetTodosResponse, "/todos/query", get_request); + // const parsed_todos = try client.post(GetTodosResponse, "/todos/query", get_request); + // defer parsed_todos.deinit(); // Example 4: Update a Todo std.debug.print("Example 4: Update a Todo\n", .{}); @@ -161,14 +164,16 @@ pub fn main() !void { } std.debug.print("\n", .{}); // In real usage: - // const update_response = try client.put(UpdateTodoResponse, "/todos/1", update_request); + // const parsed_update = try client.put(UpdateTodoResponse, "/todos/1", update_request); + // defer parsed_update.deinit(); // Example 5: Delete a Todo std.debug.print("Example 5: Delete a Todo\n", .{}); std.debug.print("------------------------\n", .{}); std.debug.print("Request: DELETE /todos/1\n", .{}); // In real usage: - // _ = try client.delete(DeleteResponse, "/todos/1"); + // const parsed_delete = try client.delete(DeleteResponse, "/todos/1"); + // defer parsed_delete.deinit(); std.debug.print("\n=== All examples completed successfully! ===\n", .{}); std.debug.print("\nTo use with a real ServiceStack API:\n", .{}); diff --git a/examples/basic.zig b/examples/basic.zig index fc662f7..c33eb1f 100644 --- a/examples/basic.zig +++ b/examples/basic.zig @@ -31,14 +31,16 @@ pub fn main() !void { // Make a POST request // Note: This is an example and will fail without a real endpoint // In a real scenario, you would use: - // const response = try client.post(HelloResponse, "/hello", request); - // std.debug.print("Response: {s}\n", .{response.result}); + // const parsed = try client.post(HelloResponse, "/hello", request); + // defer parsed.deinit(); + // std.debug.print("Response: {s}\n", .{parsed.value.result}); std.debug.print("\nJsonServiceClient initialized successfully!\n", .{}); std.debug.print("Base URL: {s}\n", .{client.base_url}); std.debug.print("Timeout: {}ms\n", .{client.timeout_ms}); std.debug.print("\nExample of how to use the client:\n", .{}); - std.debug.print(" const response = try client.post(HelloResponse, \"/hello\", request);\n", .{}); - std.debug.print(" std.debug.print(\"Result: {{s}}\\n\", .{{response.result}});\n", .{}); + std.debug.print(" const parsed = try client.post(HelloResponse, \"/hello\", request);\n", .{}); + std.debug.print(" defer parsed.deinit();\n", .{}); + std.debug.print(" std.debug.print(\"Result: {{s}}\\n\", .{{parsed.value.result}});\n", .{}); } diff --git a/examples/dtos.zig b/examples/dtos.zig index c5765c9..62a6f6c 100644 --- a/examples/dtos.zig +++ b/examples/dtos.zig @@ -280,7 +280,8 @@ pub fn main() !void { std.debug.print("Serialized CreateTodo to JSON:\n{s}\n\n", .{json_buffer.items}); std.debug.print("These DTOs would be used with JsonServiceClient like:\n", .{}); - std.debug.print(" const response = try client.post(AuthenticateResponse, \"/auth\", auth_request);\n", .{}); - std.debug.print(" const todo = try client.post(CreateTodoResponse, \"/todos\", create_todo);\n", .{}); - std.debug.print(" const todos = try client.post(GetTodosResponse, \"/todos/query\", query_todos);\n", .{}); + std.debug.print(" const parsed_auth = try client.post(AuthenticateResponse, \"/auth\", auth_request);\n", .{}); + std.debug.print(" defer parsed_auth.deinit();\n", .{}); + std.debug.print(" const parsed_todo = try client.post(CreateTodoResponse, \"/todos\", create_todo);\n", .{}); + std.debug.print(" defer parsed_todo.deinit();\n", .{}); } diff --git a/src/client.zig b/src/client.zig index 04286a5..5f0daee 100644 --- a/src/client.zig +++ b/src/client.zig @@ -32,27 +32,32 @@ pub const JsonServiceClient = struct { } /// Send a GET request and parse the response - pub fn get(self: *Self, comptime TResponse: type, path: []const u8) !TResponse { + /// Caller owns the returned Parsed(TResponse) and must call deinit() on it + pub fn get(self: *Self, comptime TResponse: type, path: []const u8) !json.Parsed(TResponse) { return self.send(TResponse, .GET, path, null); } /// Send a POST request with a request DTO and parse the response - pub fn post(self: *Self, comptime TResponse: type, path: []const u8, request: anytype) !TResponse { + /// Caller owns the returned Parsed(TResponse) and must call deinit() on it + pub fn post(self: *Self, comptime TResponse: type, path: []const u8, request: anytype) !json.Parsed(TResponse) { return self.send(TResponse, .POST, path, request); } /// Send a PUT request with a request DTO and parse the response - pub fn put(self: *Self, comptime TResponse: type, path: []const u8, request: anytype) !TResponse { + /// Caller owns the returned Parsed(TResponse) and must call deinit() on it + pub fn put(self: *Self, comptime TResponse: type, path: []const u8, request: anytype) !json.Parsed(TResponse) { return self.send(TResponse, .PUT, path, request); } /// Send a DELETE request and parse the response - pub fn delete(self: *Self, comptime TResponse: type, path: []const u8) !TResponse { + /// Caller owns the returned Parsed(TResponse) and must call deinit() on it + pub fn delete(self: *Self, comptime TResponse: type, path: []const u8) !json.Parsed(TResponse) { return self.send(TResponse, .DELETE, path, null); } /// Send a PATCH request with a request DTO and parse the response - pub fn patch(self: *Self, comptime TResponse: type, path: []const u8, request: anytype) !TResponse { + /// Caller owns the returned Parsed(TResponse) and must call deinit() on it + pub fn patch(self: *Self, comptime TResponse: type, path: []const u8, request: anytype) !json.Parsed(TResponse) { return self.send(TResponse, .PATCH, path, request); } @@ -63,7 +68,7 @@ pub const JsonServiceClient = struct { method: http.Method, path: []const u8, request: anytype, - ) !TResponse { + ) !json.Parsed(TResponse) { var client = http.Client{ .allocator = self.allocator }; defer client.deinit(); @@ -93,8 +98,11 @@ pub const JsonServiceClient = struct { try headers.append("content-type", "application/json"); // Make the request + const server_header_buffer = try self.allocator.alloc(u8, 8192); + defer self.allocator.free(server_header_buffer); + var req = try client.open(method, uri, .{ - .server_header_buffer = try self.allocator.alloc(u8, 8192), + .server_header_buffer = server_header_buffer, .headers = headers, }); defer req.deinit(); @@ -123,14 +131,13 @@ pub const JsonServiceClient = struct { } // Parse JSON response - const parsed = try json.parseFromSlice( + // Note: Caller owns the returned Parsed(TResponse) and must call deinit() on it + return try json.parseFromSlice( TResponse, self.allocator, response_buffer.items, .{ .ignore_unknown_fields = true }, ); - - return parsed.value; } }; From fd374b37a71e21d017771850f534087d7d3da387 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:30:08 +0000 Subject: [PATCH 6/7] Improve HTTP client implementation with better content-length handling and configurable response size Co-authored-by: mythz <89361+mythz@users.noreply.github.com> --- src/client.zig | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/client.zig b/src/client.zig index 5f0daee..c1ba22a 100644 --- a/src/client.zig +++ b/src/client.zig @@ -9,6 +9,7 @@ pub const JsonServiceClient = struct { allocator: mem.Allocator, base_url: []const u8, timeout_ms: u32, + max_response_size: usize, const Self = @This(); @@ -17,7 +18,8 @@ pub const JsonServiceClient = struct { return Self{ .allocator = allocator, .base_url = try allocator.dupe(u8, base_url), - .timeout_ms = 30000, // 30 second default timeout + .timeout_ms = 30000, // 30 second default timeout (note: not enforced by std.http yet) + .max_response_size = 10 * 1024 * 1024, // 10MB default max response size }; } @@ -27,10 +29,16 @@ pub const JsonServiceClient = struct { } /// Set the timeout for requests in milliseconds + /// Note: Currently not enforced by std.http.Client, reserved for future use pub fn setTimeout(self: *Self, timeout_ms: u32) void { self.timeout_ms = timeout_ms; } + /// Set the maximum response size in bytes + pub fn setMaxResponseSize(self: *Self, max_size: usize) void { + self.max_response_size = max_size; + } + /// Send a GET request and parse the response /// Caller owns the returned Parsed(TResponse) and must call deinit() on it pub fn get(self: *Self, comptime TResponse: type, path: []const u8) !json.Parsed(TResponse) { @@ -107,7 +115,12 @@ pub const JsonServiceClient = struct { }); defer req.deinit(); - req.transfer_encoding = .chunked; + // Set content length for non-empty requests + if (body_buffer) |body| { + req.transfer_encoding = .{ .content_length = body.len }; + } else { + req.transfer_encoding = .{ .content_length = 0 }; + } try req.send(); @@ -118,18 +131,18 @@ pub const JsonServiceClient = struct { try req.finish(); try req.wait(); - // Read response - var response_buffer = std.ArrayList(u8).init(self.allocator); - defer response_buffer.deinit(); - - const max_size = 10 * 1024 * 1024; // 10MB max - try req.reader().readAllArrayList(&response_buffer, max_size); - // Check status code if (req.response.status != .ok) { + // TODO: Improve error handling to include status code and response body return error.HttpError; } + // Read response + var response_buffer = std.ArrayList(u8).init(self.allocator); + defer response_buffer.deinit(); + + try req.reader().readAllArrayList(&response_buffer, self.max_response_size); + // Parse JSON response // Note: Caller owns the returned Parsed(TResponse) and must call deinit() on it return try json.parseFromSlice( From 730816ac9f7c88bfbc8f6c65c49613cd368809a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:33:19 +0000 Subject: [PATCH 7/7] Add explicit permissions to GitHub Actions workflow for security Co-authored-by: mythz <89361+mythz@users.noreply.github.com> --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b3abaa..5a6d46a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,8 @@ on: jobs: build: runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v3