├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .tool-versions ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── examples └── github │ └── main.zig └── src └── main.zig /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | fmt: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | - uses: goto-bus-stop/setup-zig@v2 16 | with: 17 | version: 0.13.0 18 | - name: fmt 19 | run: zig fmt --check . 20 | examples: 21 | runs-on: ubuntu-latest 22 | permissions: 23 | contents: write 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | - uses: goto-bus-stop/setup-zig@v2 28 | with: 29 | version: 0.13.0 30 | - name: run 31 | run: zig build run-github-example 32 | test: 33 | runs-on: ubuntu-latest 34 | permissions: 35 | contents: write 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v4 39 | - uses: goto-bus-stop/setup-zig@v2 40 | with: 41 | version: 0.13.0 42 | - name: Test 43 | run: zig build test --summary all 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *zig-* -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | # https://github.com/asdf-community/asdf-zig 2 | zig 0.13.0 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.2.2 2 | 3 | Upgrade to zig 0.13.0, current stable. No breaking changes. 4 | 5 | # 0.2.1 6 | 7 | Fix build.zig.zon package name, previously "gql" and now "graphql". 8 | 9 | # 0.2.0 10 | 11 | Upgrade to zig 0.12.0, current stable 12 | 13 | The main changes were artifacts of the [0.12.0](https://ziglang.org/download/0.12.0/release-notes.html) and build configuration changes. Because these were both breaking changes the new min supported zig version is 0.12.0. See the readme for the latest install notes. 14 | 15 | # 0.1.0 16 | 17 | Initial version 18 | 19 | ## 📼 installing 20 | 21 | ```zig 22 | .{ 23 | .name = "my-app", 24 | .version = "0.1.0", 25 | .dependencies = .{ 26 | // 👇 declare dep properties 27 | .graphql = .{ 28 | // 👇 uri to download 29 | .url = "https://github.com/softprops/zig-graphql/archive/refs/tags/v0.1.0.tar.gz", 30 | // 👇 hash verification 31 | .hash = "...", 32 | }, 33 | }, 34 | } 35 | ``` 36 | 37 | ```zig 38 | const std = @import("std"); 39 | 40 | pub fn build(b: *std.Build) void { 41 | const target = b.standardTargetOptions(.{}); 42 | 43 | const optimize = b.standardOptimizeOption(.{}); 44 | // 👇 de-reference graphql dep from build.zig.zon 45 | const graphql = b.dependency("graphql", .{ 46 | .target = target, 47 | .optimize = optimize, 48 | }); 49 | var exe = b.addExecutable(.{ 50 | .name = "your-exe", 51 | .root_source_file = .{ .path = "src/main.zig" }, 52 | .target = target, 53 | .optimize = optimize, 54 | }); 55 | // 👇 add the graphql module to executable 56 | exe.addModule("graphql", graphql.module("graphql")); 57 | 58 | b.installArtifact(exe); 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Doug Tangren 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | zig graphql 3 |

4 | 5 |
6 | A very basic GraphQL HTTP client for zig 7 |
8 | 9 | --- 10 | 11 | [![ci](https://github.com/softprops/zig-graphql/actions/workflows/ci.yml/badge.svg)](https://github.com/softprops/zig-graphql/actions/workflows/ci.yml) ![License Info](https://img.shields.io/github/license/softprops/zig-graphql) ![Releases](https://img.shields.io/github/v/release/softprops/zig-graphql) [![Zig Support](https://img.shields.io/badge/zig-0.13.0-black?logo=zig)](https://ziglang.org/documentation/0.13.0/) 12 | 13 | ## examples 14 | 15 | ```zig 16 | const std = @import("std"); 17 | const graphql = @import("graphql"); 18 | 19 | pub fn main() !void { 20 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 21 | defer _ = gpa.deinit(); 22 | const allocator = gpa.allocator(); 23 | 24 | const authz = if (std.posix.getenv("GH_TOKEN")) |pat| blk: { 25 | var buf: [400]u8 = undefined; 26 | break :blk try std.fmt.bufPrint( 27 | &buf, 28 | "bearer {s}", 29 | .{pat}, 30 | ); 31 | } else { 32 | std.log.info("Required GH_TOKEN env var containing a GitHub API token - https://github.com/settings/tokens", .{}); 33 | return; 34 | }; 35 | 36 | // 👇 constructing a client 37 | var github = try graphql.Client.init( 38 | allocator, 39 | .{ 40 | .endpoint = .{ .url = "https://api.github.com/graphql" }, 41 | .authorization = authz, 42 | }, 43 | ); 44 | defer github.deinit(); 45 | 46 | // 👇 sending a request 47 | const result = github.send( 48 | .{ 49 | .query = 50 | \\query test { 51 | \\ search(first: 100, type: REPOSITORY, query: "topic:zig") { 52 | \\ repositoryCount 53 | \\ } 54 | \\} 55 | , 56 | }, 57 | // 👇 struct representing returned data, this maybe be an adhoc or named struct 58 | // you want this to line up with the shape of your query 59 | struct { 60 | search: struct { 61 | repositoryCount: usize, 62 | }, 63 | }, 64 | ); 65 | 66 | // 👇 handle success and error 67 | if (result) |resp| { 68 | defer resp.deinit(); 69 | switch (resp.value.result()) { 70 | .data => |data| std.debug.print( 71 | "zig repo count {any}\n", 72 | .{data.search.repositoryCount}, 73 | ), 74 | .errors => |errors| { 75 | for (errors) |err| { 76 | std.debug.print("Error: {s}", .{err.message}); 77 | if (err.path) |p| { 78 | const path = try std.mem.join(allocator, "/", p); 79 | defer allocator.free(path); 80 | std.debug.print(" @ {s}", .{path}); 81 | } 82 | } 83 | }, 84 | } 85 | } else |err| { 86 | std.log.err( 87 | "Request failed with {any}", 88 | .{err}, 89 | ); 90 | } 91 | } 92 | ``` 93 | 94 | ## 📼 installing 95 | 96 | Create a new exec project with `zig init-exe`. Copy the echo handler example above into `src/main.zig` 97 | 98 | Create a `build.zig.zon` file to declare a dependency 99 | 100 | > .zon short for "zig object notation" files are essentially zig structs. `build.zig.zon` is zigs native package manager convention for where to declare dependencies 101 | 102 | Starting in zig `0.12.0`, you can use 103 | 104 | ```sh 105 | zig fetch --save https://github.com/softprops/zig-graphql/archive/refs/tags/v0.2.2.tar.gz 106 | ``` 107 | 108 | to manually add it as follows 109 | 110 | ```zig 111 | .{ 112 | .name = "my-app", 113 | .version = "0.1.0", 114 | .dependencies = .{ 115 | // 👇 declare dep properties 116 | .graphql = .{ 117 | // 👇 uri to download 118 | .url = "https://github.com/softprops/zig-graphql/archive/refs/tags/v0.2.2.tar.gz", 119 | // 👇 hash verification 120 | .hash = "{current-hash-here}", 121 | }, 122 | }, 123 | .paths = .{""}, 124 | } 125 | ``` 126 | 127 | > the hash below may vary. you can also depend any tag with `https://github.com/softprops/zig-graphql/archive/refs/tags/v{version}.tar.gz` or current main with `https://github.com/softprops/zig-graphql/archive/refs/heads/main/main.tar.gz`. to resolve a hash omit it and let zig tell you the expected value. 128 | 129 | Add the following in your `build.zig` file 130 | 131 | ```diff 132 | const std = @import("std"); 133 | 134 | pub fn build(b: *std.Build) void { 135 | const target = b.standardTargetOptions(.{}); 136 | 137 | const optimize = b.standardOptimizeOption(.{}); 138 | + // 👇 de-reference graphql dep from build.zig.zon 139 | + const graphql = b.dependency("graphql", .{ 140 | + .target = target, 141 | + .optimize = optimize, 142 | + }).module("graphql"); 143 | var exe = b.addExecutable(.{ 144 | .name = "your-exe", 145 | .root_source_file = b.path("src/main.zig"), 146 | .target = target, 147 | .optimize = optimize, 148 | }); 149 | + // 👇 add the graphql module to executable 150 | + exe.root_module.addImport("graphql", graphql); 151 | 152 | b.installArtifact(exe); 153 | } 154 | ``` 155 | 156 | ## 🥹 for budding ziglings 157 | 158 | Does this look interesting but you're new to zig and feel left out? No problem, zig is young so most us of our new are as well. Here are some resources to help get you up to speed on zig 159 | 160 | - [the official zig website](https://ziglang.org/) 161 | - [zig's one-page language documentation](https://ziglang.org/documentation/0.13.0/) 162 | - [Learning Zig](https://www.openmymind.net/learning_zig/) 163 | - [ziglings exercises](https://codeberg.org/ziglings/exercises/) 164 | 165 | \- softprops 2024 166 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | // Although this function looks imperative, note that its job is to 4 | // declaratively construct a build graph that will be executed by an external 5 | // runner. 6 | pub fn build(b: *std.Build) !void { 7 | // Standard target options allows the person running `zig build` to choose 8 | // what target to build for. Here we do not override the defaults, which 9 | // means any target is allowed, and the default is native. Other options 10 | // for restricting supported target set are available. 11 | const target = b.standardTargetOptions(.{}); 12 | 13 | // Standard optimization options allow the person running `zig build` to select 14 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not 15 | // set a preferred release mode, allowing the user to decide how to optimize. 16 | const optimize = b.standardOptimizeOption(.{}); 17 | 18 | // create a module to be used internally. 19 | const graphql = b.createModule(.{ 20 | .root_source_file = b.path("src/main.zig"), 21 | }); 22 | 23 | // register the module so it can be referenced 24 | // using the package manager. 25 | try b.modules.put(b.dupe("graphql"), graphql); 26 | 27 | // Creates a step for unit testing. This only builds the test executable 28 | // but does not run it. 29 | const main_tests = b.addTest(.{ 30 | .root_source_file = b.path("src/main.zig"), 31 | .target = target, 32 | .optimize = optimize, 33 | }); 34 | 35 | const run_main_tests = b.addRunArtifact(main_tests); 36 | 37 | // This creates a build step. It will be visible in the `zig build --help` menu, 38 | // and can be selected like this: `zig build test` 39 | // This will evaluate the `test` step rather than the default, which is "install". 40 | const test_step = b.step("test", "Run library tests"); 41 | test_step.dependOn(&run_main_tests.step); 42 | 43 | // examples (pattern inspired by zap's build.zig) 44 | inline for ([_]struct { 45 | name: []const u8, 46 | src: []const u8, 47 | }{ 48 | .{ .name = "github", .src = "examples/github/main.zig" }, 49 | }) |example| { 50 | const example_step = b.step(try std.fmt.allocPrint( 51 | b.allocator, 52 | "{s}-example", 53 | .{example.name}, 54 | ), try std.fmt.allocPrint( 55 | b.allocator, 56 | "build the {s} example", 57 | .{example.name}, 58 | )); 59 | 60 | const example_run_step = b.step(try std.fmt.allocPrint( 61 | b.allocator, 62 | "run-{s}-example", 63 | .{example.name}, 64 | ), try std.fmt.allocPrint( 65 | b.allocator, 66 | "run the {s} example", 67 | .{example.name}, 68 | )); 69 | 70 | var exe = b.addExecutable(.{ 71 | .name = example.name, 72 | .root_source_file = b.path(example.src), 73 | .target = target, 74 | .optimize = optimize, 75 | }); 76 | exe.root_module.addImport("graphql", graphql); 77 | 78 | // run the artifact - depending on the example exe 79 | const example_run = b.addRunArtifact(exe); 80 | example_run_step.dependOn(&example_run.step); 81 | 82 | // install the artifact - depending on the example exe 83 | const example_build_step = b.addInstallArtifact(exe, .{}); 84 | example_step.dependOn(&example_build_step.step); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = "graphql", 3 | .version = "0.2.2", 4 | .minimum_zig_version = "0.13.0", 5 | .paths = .{""}, 6 | } 7 | -------------------------------------------------------------------------------- /examples/github/main.zig: -------------------------------------------------------------------------------- 1 | ///! Runs a request against the GitHub GQL API 2 | ///! see [the GitHub GQL Explorer](https://docs.github.com/en/graphql/overview/explorer) to learn more about the GitHub schema 3 | const std = @import("std"); 4 | const gql = @import("graphql"); 5 | 6 | pub const std_options: std.Options = .{ 7 | .log_level = .info, // the default is .debug 8 | }; 9 | 10 | pub fn main() !void { 11 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 12 | defer _ = gpa.deinit(); 13 | const allocator = gpa.allocator(); 14 | 15 | const authz = if (std.posix.getenv("GH_TOKEN")) |pat| blk: { 16 | var buf: [400]u8 = undefined; 17 | break :blk try std.fmt.bufPrint( 18 | &buf, 19 | "bearer {s}", 20 | .{pat}, 21 | ); 22 | } else { 23 | std.log.info("Required GH_TOKEN env var containing a GitHub API token - https://github.com/settings/tokens", .{}); 24 | return; 25 | }; 26 | 27 | var github = try gql.Client.init( 28 | allocator, 29 | .{ 30 | .endpoint = .{ .url = "https://api.github.com/graphql" }, 31 | .authorization = authz, 32 | }, 33 | ); 34 | defer github.deinit(); 35 | 36 | const result = github.send( 37 | .{ 38 | .query = 39 | \\query test { 40 | \\ search(first: 100, type: REPOSITORY, query: "topic:zig") { 41 | \\ repositoryCount 42 | \\ } 43 | \\} 44 | , 45 | }, 46 | // 👇 struct representing returned data, this maybe be an adhoc or named struct 47 | // you want this to line up with the shape of your query 48 | struct { 49 | search: struct { 50 | repositoryCount: usize, 51 | }, 52 | }, 53 | ); 54 | 55 | // handle success and error 56 | if (result) |resp| { 57 | defer resp.deinit(); 58 | switch (resp.value.result()) { 59 | .data => |data| std.debug.print( 60 | "zig repo count {any}\n", 61 | .{data.search.repositoryCount}, 62 | ), 63 | .errors => |errors| { 64 | for (errors) |err| { 65 | std.debug.print("Error: {s}", .{err.message}); 66 | if (err.path) |p| { 67 | const path = try std.mem.join(allocator, "/", p); 68 | defer allocator.free(path); 69 | std.debug.print(" @ {s}", .{path}); 70 | } 71 | } 72 | }, 73 | } 74 | } else |err| { 75 | std.log.err( 76 | "Request failed with {any}", 77 | .{err}, 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | /// A very basic general purpose GraphQL HTTP client 2 | const std = @import("std"); 3 | 4 | pub const Endpoint = union(enum) { 5 | url: []const u8, 6 | uri: std.Uri, 7 | 8 | fn toUri(self: @This()) std.Uri.ParseError!std.Uri { 9 | return switch (self) { 10 | .url => |u| try std.Uri.parse(u), 11 | .uri => |u| u, 12 | }; 13 | } 14 | }; 15 | 16 | /// Represents a standard GraphQL request 17 | /// 18 | /// see also these [GraphQL docs](https://graphql.org/learn/serving-over-http/#post-request) 19 | pub const Request = struct { 20 | query: []const u8, 21 | operationName: ?[]const u8 = null, 22 | }; 23 | 24 | pub const Location = struct { 25 | line: usize, 26 | column: usize, 27 | }; 28 | 29 | /// Represents a standard GraphQL response error 30 | /// 31 | /// See the [GraphQL docs](https://spec.graphql.org/October2021/#sec-Errors.Error-result-format) for more information 32 | pub const Error = struct { 33 | message: []const u8, 34 | path: ?[][]const u8 = null, 35 | locations: ?[]Location = null, 36 | }; 37 | 38 | /// Represents a standard GraphQL response which may contain data or errors. Use the `result` method to dereference this for the common usecase 39 | /// of one or the other but not both 40 | /// 41 | /// See the [GraphQL docs](https://graphql.org/learn/serving-over-http/#response) for more information 42 | pub fn Response(comptime T: type) type { 43 | return struct { 44 | errors: ?[]Error = null, 45 | data: ?T = null, 46 | 47 | /// a union of data or errors 48 | const Result = union(enum) { data: T, errors: []Error }; 49 | 50 | /// simplifies the ease of processing the presence and/or absence of data or errors 51 | /// in a unified type. This method assumes the response contains one or the other. 52 | pub fn result(self: @This()) Result { 53 | if (self.data) |data| { 54 | return .{ .data = data }; 55 | } else if (self.errors) |errors| { 56 | return .{ .errors = errors }; 57 | } else { 58 | unreachable; 59 | } 60 | } 61 | }; 62 | } 63 | 64 | /// Client options 65 | pub const Options = struct { 66 | /// HTTP url for graphql endpoint 67 | endpoint: Endpoint, 68 | /// HTTP authorization header contents 69 | authorization: ?[]const u8, 70 | }; 71 | 72 | /// Possible request errors 73 | const RequestError = error{ 74 | /// the request was not authorized by server 75 | NotAuthorized, 76 | /// the request was forbidden by server 77 | Forbidden, 78 | /// the server returned an unxpected result 79 | ServerError, 80 | /// there was an http network or client error 81 | Http, 82 | /// there was a problem serializing the request 83 | Serialization, 84 | /// there was a problem deserizalize the response 85 | Deserialization, 86 | /// the server throttled the request 87 | Throttled, 88 | }; 89 | 90 | /// A type that expresses the caller's ownership responsiblity to deinitailize the data. 91 | pub fn Owned(comptime T: type) type { 92 | return struct { 93 | value: T, 94 | arena: *std.heap.ArenaAllocator, 95 | 96 | const Self = @This(); 97 | 98 | fn fromJson(parsed: std.json.Parsed(T)) Self { 99 | return .{ 100 | .arena = parsed.arena, 101 | .value = parsed.value, 102 | }; 103 | } 104 | 105 | pub fn deinit(self: Self) void { 106 | const arena = self.arena; 107 | const allocator = arena.child_allocator; 108 | arena.deinit(); 109 | allocator.destroy(arena); 110 | } 111 | }; 112 | } 113 | 114 | /// A simple GraphQL HTTP client 115 | pub const Client = struct { 116 | httpClient: std.http.Client, 117 | allocator: std.mem.Allocator, 118 | options: Options, 119 | const Self = @This(); 120 | 121 | /// Initializes a new GraphQL Client. Be sure to call `deinit` when finished 122 | /// using this instance 123 | pub fn init( 124 | allocator: std.mem.Allocator, 125 | options: Options, 126 | ) std.Uri.ParseError!Self { 127 | // validate that Uri is validate as early as possible 128 | _ = try options.endpoint.toUri(); 129 | return .{ 130 | .httpClient = std.http.Client{ .allocator = allocator }, 131 | .allocator = allocator, 132 | .options = options, 133 | }; 134 | } 135 | 136 | /// Call this method to deallocate resources 137 | pub fn deinit(self: *Self) void { 138 | self.httpClient.deinit(); 139 | } 140 | 141 | /// Sends a GraphQL Request to a server 142 | /// 143 | /// Callers are expected to call `deinit()` on the Owned type returned to free memory. 144 | pub fn send( 145 | self: *Self, 146 | request: Request, 147 | comptime T: type, 148 | ) RequestError!Owned(Response(T)) { 149 | const headers = std.http.Client.Request.Headers{ 150 | .content_type = .{ .override = "application/json" }, 151 | .authorization = if (self.options.authorization) |authz| .{ 152 | .override = authz, 153 | } else .default, 154 | }; 155 | 156 | // same as std client.fetch(...) default server_header_buffer 157 | var server_header_buffer: [16 * 1024]u8 = undefined; 158 | var req = self.httpClient.open( 159 | .POST, 160 | // endpoint is validated on client init 161 | self.options.endpoint.toUri() catch unreachable, 162 | .{ 163 | .headers = headers, 164 | .server_header_buffer = &server_header_buffer, 165 | }, 166 | ) catch return error.Http; 167 | defer req.deinit(); 168 | req.transfer_encoding = .chunked; 169 | req.send() catch return error.Http; 170 | serializeRequest(request, req.writer()) catch return error.Serialization; 171 | req.finish() catch return error.Http; 172 | req.wait() catch return error.Http; 173 | switch (req.response.status.class()) { 174 | // client errors 175 | .client_error => switch (req.response.status) { 176 | .unauthorized => return error.NotAuthorized, 177 | .forbidden => return error.Forbidden, 178 | else => return error.Http, 179 | }, 180 | // handle server errors 181 | .server_error => return error.ServerError, 182 | // handle "success" 183 | else => { 184 | std.log.debug("response {any}", .{req.response.status}); 185 | const body = req.reader().readAllAlloc( 186 | self.allocator, 187 | 8192 * 2 * 2, // note: optimistic arb choice of buffer size 188 | ) catch unreachable; 189 | defer self.allocator.free(body); 190 | const parsed = parseResponse(self.allocator, body, T) catch return error.Deserialization; 191 | return Owned(Response(T)).fromJson(parsed); 192 | }, 193 | } 194 | } 195 | }; 196 | 197 | fn serializeRequest(request: Request, writer: anytype) @TypeOf(writer).Error!void { 198 | try std.json.stringify( 199 | request, 200 | .{ .emit_null_optional_fields = false }, 201 | writer, 202 | ); 203 | } 204 | 205 | fn parseResponse( 206 | allocator: std.mem.Allocator, 207 | body: []const u8, 208 | comptime T: type, 209 | ) std.json.ParseError(std.json.Scanner)!std.json.Parsed(Response(T)) { 210 | std.log.debug("parsing body {s}\n", .{body}); 211 | return try std.json.parseFromSlice( 212 | Response(T), 213 | allocator, 214 | body, 215 | .{ 216 | .ignore_unknown_fields = true, 217 | .allocate = .alloc_always, // nested structures are known to segfault with the default 218 | }, 219 | ); 220 | } 221 | 222 | test "serialize request" { 223 | var buf: [1024]u8 = undefined; 224 | var fbs = std.io.fixedBufferStream(&buf); 225 | const tests = [_]struct { 226 | request: Request, 227 | expect: []const u8, 228 | }{ 229 | .{ 230 | .request = .{ 231 | .query = 232 | \\ { 233 | \\ foo 234 | \\ } 235 | , 236 | }, 237 | .expect = 238 | \\{"query":" {\n foo\n }"} 239 | , 240 | }, 241 | .{ 242 | .request = .{ 243 | .query = 244 | \\ { 245 | \\ foo 246 | \\ } 247 | , 248 | .operationName = "foo", 249 | }, 250 | .expect = 251 | \\{"query":" {\n foo\n }","operationName":"foo"} 252 | , 253 | }, 254 | }; 255 | for (tests) |t| { 256 | defer fbs.reset(); 257 | try serializeRequest(t.request, fbs.writer()); 258 | try std.testing.expectEqualStrings(t.expect, fbs.getWritten()); 259 | } 260 | } 261 | 262 | test "parse response" { 263 | const allocator = std.testing.allocator; 264 | const T = struct { 265 | foo: []const u8, 266 | }; 267 | var path = [_][]const u8{ 268 | "foo", 269 | "bar", 270 | }; 271 | const err = Error{ 272 | .message = "err", 273 | .path = &path, 274 | }; 275 | var errors = [_]Error{ 276 | err, 277 | }; 278 | const tests = [_]struct { 279 | body: []const u8, 280 | result: anyerror!Response(T), 281 | }{ 282 | .{ 283 | .body = 284 | \\{ 285 | \\ "data": { 286 | \\ "foo": "success" 287 | \\ } 288 | \\} 289 | , 290 | .result = .{ 291 | .data = .{ 292 | .foo = "success", 293 | }, 294 | }, 295 | }, 296 | .{ 297 | .body = 298 | \\{ 299 | \\ "errors": [{ 300 | \\ "message": "err", 301 | \\ "path": ["foo","bar"] 302 | \\ }] 303 | \\} 304 | , 305 | .result = .{ 306 | .errors = &errors, 307 | }, 308 | }, 309 | }; 310 | 311 | for (tests) |t| { 312 | const result = try parseResponse(allocator, t.body, T); 313 | defer result.deinit(); 314 | try std.testing.expectEqualDeep(t.result, result.value); 315 | } 316 | } 317 | 318 | test "response" { 319 | const err = Error{ 320 | .message = "err", 321 | }; 322 | var errors = [_]Error{ 323 | err, 324 | }; 325 | const tests = [_]struct { 326 | response: Response(u32), 327 | result: Response(u32).Result, 328 | }{ 329 | .{ 330 | .response = .{ .data = 42 }, 331 | .result = .{ .data = 42 }, 332 | }, 333 | .{ 334 | .response = .{ .errors = &errors }, 335 | .result = .{ .errors = &errors }, 336 | }, 337 | }; 338 | 339 | for (tests) |t| { 340 | try std.testing.expectEqualDeep(t.result, t.response.result()); 341 | } 342 | } 343 | --------------------------------------------------------------------------------