├── .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 | [](https://github.com/softprops/zig-graphql/actions/workflows/ci.yml)   [](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 |
--------------------------------------------------------------------------------