├── data ├── msg_error1.json ├── msg_error2.json ├── hello.json ├── hello_end.json ├── hello_end_server.json ├── calc_dec.json ├── calc_inc.json ├── msg_error3.json ├── calc_add.json ├── calc_load.json ├── calc_pow.json ├── hello_optional_text2.json ├── mcp_tools_list.json ├── calc_divide.json ├── calc_lognum.json ├── calc_sub.json ├── hello_say.json ├── msg_error4.json ├── calc_divide_99.json ├── calc_divide_by_0.json ├── calc_multiply.json ├── calc_save.json ├── hello_cl.json ├── hello_end_cl.json ├── hello_optional_text3.json ├── calc_pow_invalid_type.json ├── hello_name.json ├── hello_substr.json ├── calc_make_cat.json ├── hello_optional_text.json ├── hello_xtimes.json ├── mcp_tools_list_cursor.json ├── hello_end_server_cl.json ├── mcp_notification_initialized.json ├── mcp_tool_call_hello.json ├── hello_say_cl.json ├── hello_name_cl.json ├── calc_clone_cat.json ├── calc_desc_cat.json ├── calc_weigh_cat1.json ├── calc_weigh_cat2.json ├── calc_weigh_odin.json ├── calc_weigh_garfield.json ├── calc_add_weight.json ├── mcp_initialize.json ├── calc_batch.json ├── calc_inc_batch.json ├── calc_save_batch.json ├── dispatcher_counter.json ├── hello_stream.json ├── dispatcher_hello.json ├── stream_calc_by_length.json └── stream_calc_by_lf.json ├── .gitignore ├── LICENSE ├── src ├── jsonrpc │ ├── errors.zig │ ├── message.zig │ ├── response.zig │ ├── composer.zig │ └── request.zig ├── rpc │ ├── deiniter.zig │ ├── logger.zig │ ├── rpc_dispatcher.zig │ └── dispatcher.zig ├── zigjr.zig ├── tests │ ├── frame_tests.zig │ ├── misc_tests.zig │ └── message_tests.zig └── streaming │ ├── BufReader.zig │ ├── DupWriter.zig │ ├── frame.zig │ └── stream.zig ├── examples ├── hello_single.zig ├── hello.zig ├── dispatcher_counter.zig ├── dispatcher_hello.zig ├── calc.zig ├── stream_calc.zig ├── hello_net.zig ├── lsp_client.zig └── mcp_hello.zig └── todo-zigjr.org /data/msg_error1.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | -------------------------------------------------------------------------------- /data/msg_error2.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | 4 | -------------------------------------------------------------------------------- /data/hello.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "hello", "id": 1} 2 | -------------------------------------------------------------------------------- /data/hello_end.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "end-session"} 2 | -------------------------------------------------------------------------------- /data/hello_end_server.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "end-server"} 2 | -------------------------------------------------------------------------------- /data/calc_dec.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "dec", "params": [4], "id": 1} 2 | -------------------------------------------------------------------------------- /data/calc_inc.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "inc", "params": [5], "id": 1} 2 | -------------------------------------------------------------------------------- /data/msg_error3.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "id": 2 4 | } 5 | 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache 2 | zig-cache 3 | zig-out 4 | log.txt 5 | ~* 6 | *~ 7 | *# 8 | -------------------------------------------------------------------------------- /data/calc_add.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "add", "params": [12, 30], "id": 1} 2 | -------------------------------------------------------------------------------- /data/calc_load.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "load", "params": ["foo"], "id": 1} 2 | -------------------------------------------------------------------------------- /data/calc_pow.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "pow", "params": [2.5, 2], "id": 1} 2 | -------------------------------------------------------------------------------- /data/hello_optional_text2.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "opt-text", "id": 1} 2 | -------------------------------------------------------------------------------- /data/mcp_tools_list.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}} 2 | -------------------------------------------------------------------------------- /data/calc_divide.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "divide", "params": [42, 6], "id": 1} 2 | -------------------------------------------------------------------------------- /data/calc_lognum.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "logNum", "params": [123], "id": 1} 2 | -------------------------------------------------------------------------------- /data/calc_sub.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "subtract", "params": [12, 30], "id": 1} 2 | -------------------------------------------------------------------------------- /data/hello_say.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "say", "params": ["Good day."], "id": 1} 2 | -------------------------------------------------------------------------------- /data/msg_error4.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 123456789012345678901234567890 3 | } 4 | 5 | 6 | -------------------------------------------------------------------------------- /data/calc_divide_99.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "divide", "params": [99, 2], "id": 1} 2 | -------------------------------------------------------------------------------- /data/calc_divide_by_0.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "divide", "params": [42, 0], "id": 1} 2 | -------------------------------------------------------------------------------- /data/calc_multiply.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "multiply", "params": [6, 7], "id": 1} 2 | -------------------------------------------------------------------------------- /data/calc_save.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "save", "params": ["foo", 15.6], "id": 1} 2 | -------------------------------------------------------------------------------- /data/hello_cl.json: -------------------------------------------------------------------------------- 1 | Content-Length: 46 2 | 3 | {"jsonrpc": "2.0", "method": "hello", "id": 1} 4 | -------------------------------------------------------------------------------- /data/hello_end_cl.json: -------------------------------------------------------------------------------- 1 | Content-Length: 43 2 | 3 | {"jsonrpc": "2.0", "method": "end-session"} 4 | -------------------------------------------------------------------------------- /data/hello_optional_text3.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "opt-text", "params": [], "id": 1} 2 | -------------------------------------------------------------------------------- /data/calc_pow_invalid_type.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "pow", "params": [2.5, 2.0], "id": 1} 2 | -------------------------------------------------------------------------------- /data/hello_name.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "hello-name", "params": ["Spiderman"], "id": 2} 2 | -------------------------------------------------------------------------------- /data/hello_substr.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "substr", "params": ["Foobar", 1, 3], "id": 1} 2 | -------------------------------------------------------------------------------- /data/calc_make_cat.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "make-cat", "params": ["Garfield", "red"], "id": 1} 2 | -------------------------------------------------------------------------------- /data/hello_optional_text.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "opt-text", "params": ["Good day."], "id": 1} 2 | -------------------------------------------------------------------------------- /data/hello_xtimes.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "hello-xtimes", "params": ["Foobar", 5], "id": 1} 2 | -------------------------------------------------------------------------------- /data/mcp_tools_list_cursor.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{"cursor":"2"}} 2 | -------------------------------------------------------------------------------- /data/hello_end_server_cl.json: -------------------------------------------------------------------------------- 1 | Content-Length: 42 2 | 3 | {"jsonrpc": "2.0", "method": "end-server"} 4 | 5 | -------------------------------------------------------------------------------- /data/mcp_notification_initialized.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc":"2.0","method":"notifications/initialized","params":{}} 2 | -------------------------------------------------------------------------------- /data/mcp_tool_call_hello.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"hello","arguments":{}}} 2 | -------------------------------------------------------------------------------- /data/hello_say_cl.json: -------------------------------------------------------------------------------- 1 | Content-Length: 69 2 | 3 | {"jsonrpc": "2.0", "method": "say", "params": ["Good day."], "id": 1} 4 | -------------------------------------------------------------------------------- /data/hello_name_cl.json: -------------------------------------------------------------------------------- 1 | Content-Length: 76 2 | 3 | {"jsonrpc": "2.0", "method": "hello-name", "params": ["Spiderman"], "id": 2} 4 | -------------------------------------------------------------------------------- /data/calc_clone_cat.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "clone-cat", "params": {"cat_name":"Cat1","weight":4.0,"eye_color":"brown"}, "id": 1} 2 | -------------------------------------------------------------------------------- /data/calc_desc_cat.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "desc-cat", "params": {"cat_name":"Cat1","weight":4.0,"eye_color":"brown"}, "id": 1} 2 | -------------------------------------------------------------------------------- /data/calc_weigh_cat1.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "weigh-cat", "params": {"cat_name":"cat1","weight":7.5,"eye_color":"brown"}, "id": 1} 2 | -------------------------------------------------------------------------------- /data/calc_weigh_cat2.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "weigh-cat", "params": {"cat_name":"cat2","weight":14.0,"eye_color":"brown"}, "id": 1} 2 | -------------------------------------------------------------------------------- /data/calc_weigh_odin.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "weigh-cat", "params": {"cat_name":"Odin","weight":11.0,"eye_color":"pink"}, "id": 1} 2 | -------------------------------------------------------------------------------- /data/calc_weigh_garfield.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "weigh-cat", "params": {"cat_name":"Garfield","weight":18.0,"eye_color":"red"}, "id": 1} 2 | -------------------------------------------------------------------------------- /data/calc_add_weight.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "add-weight", "params": [2.2, {"cat_name":"Garfield","weight":18.0,"eye_color":"red"}], "id": 1} 2 | -------------------------------------------------------------------------------- /data/mcp_initialize.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"mcphost","version":"1.0.0"},"capabilities":{}}} 2 | -------------------------------------------------------------------------------- /data/calc_batch.json: -------------------------------------------------------------------------------- 1 | [{"jsonrpc": "2.0", "method": "inc", "params": [2], "id": 16}, {"jsonrpc": "2.0", "method": "inc", "params": [1], "id": 17}, {"jsonrpc": "2.0", "method": "inc", "params": [3], "id": 18}] 2 | -------------------------------------------------------------------------------- /data/calc_inc_batch.json: -------------------------------------------------------------------------------- 1 | [{"jsonrpc": "2.0", "method": "inc", "params": [2], "id": 1}, {"jsonrpc": "2.0", "method": "inc", "params": [1], "id": 1}, {"jsonrpc": "2.0", "method": "inc", "params": [3], "id": 1}] 2 | -------------------------------------------------------------------------------- /data/calc_save_batch.json: -------------------------------------------------------------------------------- 1 | [{"jsonrpc": "2.0", "method": "load", "params": ["foo"], "id": 1}, {"jsonrpc": "2.0", "method": "save", "params": ["foo", 15.6], "id": 1}, {"jsonrpc": "2.0", "method": "load", "params": ["foo"], "id": 1}] 2 | -------------------------------------------------------------------------------- /data/dispatcher_counter.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "inc", "params": [2], "id": 1} 2 | {"jsonrpc": "2.0", "method": "get", "params": [], "id": 3} 3 | {"jsonrpc": "2.0", "method": "inc", "params": [1], "id": 4} 4 | {"jsonrpc": "2.0", "method": "get", "params": [], "id": 5} 5 | -------------------------------------------------------------------------------- /data/hello_stream.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "hello", "id": 1} 2 | {"jsonrpc": "2.0", "method": "hello-name", "params": ["Foo"], "id": 2} 3 | {"jsonrpc": "2.0", "method": "hello-name", "params": ["Bar"], "id": 3} 4 | {"jsonrpc": "2.0", "method": "hello-name", "params": ["Baz"], "id": 4} 5 | {"jsonrpc": "2.0", "method": "hello-xtimes", "params": ["Foobar", 2], "id": 5} 6 | {"jsonrpc": "2.0", "method": "hello-xtimes", "params": ["Spiderman", 3], "id": 6} 7 | {"jsonrpc": "2.0", "method": "say", "params": ["Logging message without returning result."], "id": 7} 8 | {"jsonrpc": "2.0", "method": "say", "params": ["Good day."], "id": 8} 9 | {"jsonrpc": "2.0", "method": "hello", "id": 9} 10 | -------------------------------------------------------------------------------- /data/dispatcher_hello.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "hello", "id": 1} 2 | {"jsonrpc": "2.0", "method": "hello-name", "params": ["Foo"], "id": 2} 3 | {"jsonrpc": "2.0", "method": "hello-name", "params": ["Bar"], "id": 3} 4 | {"jsonrpc": "2.0", "method": "hello-name", "params": ["Baz"], "id": 4} 5 | {"jsonrpc": "2.0", "method": "hello-xtimes", "params": ["Foobar", 2], "id": 5} 6 | {"jsonrpc": "2.0", "method": "hello-xtimes", "params": ["Spiderman", 3], "id": 6} 7 | {"jsonrpc": "2.0", "method": "say", "params": ["Logging message without returning result."], "id": 7} 8 | {"jsonrpc": "2.0", "method": "say", "params": ["Good day."], "id": 8} 9 | {"jsonrpc": "2.0", "method": "hello", "id": 9} 10 | -------------------------------------------------------------------------------- /data/stream_calc_by_length.json: -------------------------------------------------------------------------------- 1 | Content-Length: 64 2 | 3 | {"jsonrpc": "2.0", "method": "add", "params": [12, 30], "id": 1} 4 | Content-Length: 69 5 | 6 | {"jsonrpc": "2.0", "method": "subtract", "params": [12, 30], "id": 2} 7 | Content-Length: 67 8 | 9 | {"jsonrpc": "2.0", "method": "multiply", "params": [6, 7], "id": 3} 10 | Content-Length: 66 11 | 12 | {"jsonrpc": "2.0", "method": "divide", "params": [42, 6], "id": 4} 13 | Content-Length: 66 14 | 15 | {"jsonrpc": "2.0", "method": "divide", "params": [42, 0], "id": 5} 16 | Content-Length: 66 17 | 18 | {"jsonrpc": "2.0", "method": "divide", "params": [99, 2], "id": 6} 19 | 20 | 21 | 22 | Content-Length: 64 23 | 24 | {"jsonrpc": "2.0", "method": "pow", "params": [2.5, 2], "id": 7} 25 | Content-Length: 66 26 | 27 | {"jsonrpc": "2.0", "method": "pow", "params": [2.5, 2.0], "id": 8} 28 | 29 | Content-Length: 186 30 | 31 | [{"jsonrpc": "2.0", "method": "inc", "params": [2], "id": 16}, {"jsonrpc": "2.0", "method": "inc", "params": [1], "id": 17}, {"jsonrpc": "2.0", "method": "inc", "params": [3], "id": 18}] 32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Zig JR 2 | A Zig based JSON-RPC 2.0 library. 3 | Copyright (C) 2025 William W. Wong. All rights reserved. 4 | (williamw520@gmail.com) 5 | 6 | MIT License 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /src/jsonrpc/errors.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const std = @import("std"); 10 | 11 | // JSON-RPC 2.0 error codes. 12 | pub const ErrorCode = enum(i32) { 13 | None = 0, 14 | ParseError = -32700, // Invalid JSON was received by the server. 15 | InvalidRequest = -32600, // The JSON sent is not a valid Request object. 16 | MethodNotFound = -32601, // The method does not exist / is not available. 17 | InvalidParams = -32602, // Invalid method parameter(s). 18 | InternalError = -32603, // Internal JSON-RPC error. 19 | ServerError = -32000, // -32000 to -32099 reserved for implementation defined errors. 20 | }; 21 | 22 | pub const JrErrors = error { 23 | NotSingleRpcRequest, 24 | NotBatchRpcRequest, 25 | NotSingleRpcResponse, 26 | NotBatchRpcResponse, 27 | NotArray, 28 | NotObject, 29 | MissingIdForResponse, 30 | NotResultResponse, 31 | NotErrResponse, 32 | InvalidResponse, 33 | InvalidParamsType, 34 | InvalidParamType, 35 | InvalidJsonValueType, 36 | InvalidJsonRpcversion, 37 | MissingContentLengthHeader, 38 | InvalidRpcIdValueType, 39 | UnsupportedParamType, 40 | RequiredI64Integer, 41 | RequiredF64Float, 42 | RequiredU8SliceForString, 43 | RequiredU8ArrayForString, 44 | ResponseHasError, 45 | } || WriteAllocError; 46 | 47 | pub const WriteAllocError = error{ 48 | WriteFailed, // std.Io.Writer.Error.WriteFailed 49 | OutOfMemory, 50 | }; 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/jsonrpc/message.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const std = @import("std"); 10 | const Type = std.builtin.Type; 11 | const Allocator = std.mem.Allocator; 12 | const Parsed = std.json.Parsed; 13 | const Scanner = std.json.Scanner; 14 | const ParseOptions = std.json.ParseOptions; 15 | const innerParse = std.json.innerParse; 16 | const ParseError = std.json.ParseError; 17 | const Value = std.json.Value; 18 | const Array = std.json.Array; 19 | const ObjectMap = std.json.ObjectMap; 20 | 21 | const errors = @import("errors.zig"); 22 | const ErrorCode = errors.ErrorCode; 23 | const JrErrors = errors.JrErrors; 24 | 25 | const req = @import("request.zig"); 26 | const res = @import("response.zig"); 27 | 28 | 29 | 30 | pub fn parseRpcMessage(alloc: Allocator, json_str: []const u8) RpcMessageResult { 31 | if (std.mem.indexOf(u8, json_str, "\"method\":")) |_| { 32 | const req_result = req.parseRpcRequest(alloc, json_str); 33 | if (!req_result.isMissingMethod()) { 34 | return .{ .request_result = req_result }; // a valid request or a non-missing-method error. 35 | } 36 | } 37 | return .{ .response_result = res.parseRpcResponse(alloc, json_str) }; 38 | } 39 | 40 | pub const RpcMessageResult = union(enum) { 41 | request_result: req.RpcRequestResult, 42 | response_result: res.RpcResponseResult, 43 | 44 | pub fn deinit(self: *@This()) void { 45 | switch (self.*) { 46 | .request_result => |*rr| rr.deinit(), 47 | .response_result => |*rr| rr.deinit(), 48 | } 49 | } 50 | }; 51 | 52 | -------------------------------------------------------------------------------- /src/rpc/deiniter.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const std = @import("std"); 10 | 11 | 12 | /// Capture the calling of an object's deinit() in the uniform Deiniter object. 13 | pub const Deiniter = struct { 14 | impl_ptr: *anyopaque, 15 | deinit_fn: *const fn(impl_ptr: *anyopaque) void, 16 | 17 | pub fn implBy(impl_obj: anytype) @This() { 18 | const Delegate = struct { 19 | fn deinit(impl_ptr: *anyopaque) void { 20 | const impl: @TypeOf(impl_obj) = @ptrCast(@alignCast(impl_ptr)); 21 | impl.deinit(); 22 | } 23 | }; 24 | 25 | return .{ 26 | .impl_ptr = impl_obj, 27 | .deinit_fn = Delegate.deinit, 28 | }; 29 | } 30 | 31 | pub fn deinit(self: *@This()) void { 32 | self.deinit_fn(self.impl_ptr); 33 | } 34 | }; 35 | 36 | /// Capture the calling of an object's deinit() in the uniform Deiniter object. 37 | pub const ConstDeiniter = struct { 38 | impl_ptr: *const anyopaque, 39 | deinit_fn: *const fn(impl_ptr: *const anyopaque) void, 40 | 41 | pub fn implBy(impl_obj: anytype) @This() { 42 | const Delegate = struct { 43 | fn deinit(impl_ptr: *const anyopaque) void { 44 | const impl: @TypeOf(impl_obj) = @ptrCast(@alignCast(impl_ptr)); 45 | impl.deinit(); 46 | } 47 | }; 48 | 49 | return .{ 50 | .impl_ptr = impl_obj, 51 | .deinit_fn = Delegate.deinit, 52 | }; 53 | } 54 | 55 | pub fn deinit(self: @This()) void { 56 | self.deinit_fn(self.impl_ptr); 57 | } 58 | }; 59 | 60 | /// Capture the memory and its allocator in one place so it can be freed later. 61 | pub fn Owned(T: type) type { 62 | return struct { 63 | memory: ?T = null, 64 | alloc: std.mem.Allocator = undefined, 65 | 66 | pub fn init(memory: ?T, alloc: std.mem.Allocator) @This() { 67 | return .{ 68 | .memory = memory, 69 | .alloc = alloc, 70 | }; 71 | } 72 | 73 | pub fn deinit(self: *@This()) void { 74 | if (self.memory) |mem_ptr| self.alloc.free(mem_ptr); 75 | } 76 | }; 77 | } 78 | 79 | 80 | -------------------------------------------------------------------------------- /data/stream_calc_by_lf.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc": "2.0", "method": "add", "params": [12, 30], "id": 1} 2 | {"jsonrpc": "2.0", "method": "subtract", "params": [12, 30], "id": 2} 3 | {"jsonrpc": "2.0", "method": "multiply", "params": [6, 7], "id": 3} 4 | {"jsonrpc": "2.0", "method": "divide", "params": [42, 6], "id": 4} 5 | {"jsonrpc": "2.0", "method": "divide", "params": [42, 0], "id": 5} 6 | {"jsonrpc": "2.0", "method": "divide", "params": [99, 2], "id": 6} 7 | {"jsonrpc": "2.0", "method": "pow", "params": [2.5, 2], "id": 7} 8 | {"jsonrpc": "2.0", "method": "pow", "params": [2.5, 2.0], "id": 8} 9 | 10 | {"jsonrpc": "2.0", "method": "logNum", "params": [123], "id": 9} 11 | 12 | {"jsonrpc": "2.0", "method": "inc", "params": [5], "id": 11} 13 | {"jsonrpc": "2.0", "method": "inc", "params": [1], "id": 12} 14 | {"jsonrpc": "2.0", "method": "inc", "params": [1], "id": 13} 15 | {"jsonrpc": "2.0", "method": "dec", "params": [1], "id": 14} 16 | {"jsonrpc": "2.0", "method": "dec", "params": [1], "id": 15} 17 | [{"jsonrpc": "2.0", "method": "inc", "params": [2], "id": 16}, {"jsonrpc": "2.0", "method": "inc", "params": [1], "id": 17}, {"jsonrpc": "2.0", "method": "inc", "params": [3], "id": 18}] 18 | 19 | {"jsonrpc": "2.0", "method": "load", "params": ["foo"], "id": 21} 20 | {"jsonrpc": "2.0", "method": "save", "params": ["foo", 15.6], "id": 22} 21 | {"jsonrpc": "2.0", "method": "load", "params": ["foo"], "id": 23} 22 | [{"jsonrpc": "2.0", "method": "load", "params": ["foo"], "id": 24}, {"jsonrpc": "2.0", "method": "save", "params": ["foo", 15.6], "id": 25}, {"jsonrpc": "2.0", "method": "load", "params": ["foo"], "id": 26}] 23 | 24 | {"jsonrpc": "2.0", "method": "make-cat", "params": ["Garfield", "red"], "id": 31} 25 | {"jsonrpc": "2.0", "method": "add-weight", "params": [2.2, {"cat_name":"Garfield","weight":18.0,"eye_color":"red"}], "id": 32} 26 | {"jsonrpc": "2.0", "method": "clone-cat", "params": {"cat_name":"Cat1","weight":4.0,"eye_color":"brown"}, "id": 33} 27 | {"jsonrpc": "2.0", "method": "desc-cat", "params": {"cat_name":"Cat1","weight":4.0,"eye_color":"brown"}, "id": 34} 28 | {"jsonrpc": "2.0", "method": "weigh-cat", "params": {"cat_name":"cat1","weight":7.5,"eye_color":"brown"}, "id": 35} 29 | {"jsonrpc": "2.0", "method": "weigh-cat", "params": {"cat_name":"cat2","weight":14.0,"eye_color":"brown"}, "id": 36} 30 | {"jsonrpc": "2.0", "method": "weigh-cat", "params": {"cat_name":"Garfield","weight":18.0,"eye_color":"red"}, "id": 37} 31 | {"jsonrpc": "2.0", "method": "weigh-cat", "params": {"cat_name":"Odin","weight":11.0,"eye_color":"pink"}, "id": 38} 32 | 33 | -------------------------------------------------------------------------------- /examples/hello_single.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const std = @import("std"); 10 | const Allocator = std.mem.Allocator; 11 | 12 | const zigjr = @import("zigjr"); 13 | 14 | 15 | pub fn main() !void { 16 | var gpa = std.heap.DebugAllocator(.{}){}; 17 | defer _ = gpa.deinit(); 18 | const alloc = gpa.allocator(); 19 | 20 | // Create a registry for the JSON-RPC registry. 21 | var rpc_dispatcher = try zigjr.RpcDispatcher.init(alloc); 22 | defer rpc_dispatcher.deinit(); 23 | 24 | // Register each RPC method with a handling function. 25 | try rpc_dispatcher.add("hello", hello); 26 | try rpc_dispatcher.add("hello-name", helloName); 27 | try rpc_dispatcher.add("hello-xtimes", helloXTimes); 28 | try rpc_dispatcher.add("say", say); 29 | 30 | // RequestDispatcher interface implemented by the 'registry' registry. 31 | const dispatcher = zigjr.RequestDispatcher.implBy(&rpc_dispatcher); 32 | var pipeline = try zigjr.RequestPipeline.init(alloc, dispatcher, null); 33 | defer pipeline.deinit(); 34 | 35 | // Read a JSON-RPC request JSON from StdIn. 36 | var stdin_buffer: [256]u8 = undefined; 37 | var stdin_reader = std.fs.File.stdin().reader(&stdin_buffer); 38 | const stdin = &stdin_reader.interface; 39 | var read_buf = std.Io.Writer.Allocating.init(alloc); 40 | defer read_buf.deinit(); 41 | const read_len = stdin.streamDelimiter(&read_buf.writer, '\n') catch |err| blk: { 42 | switch (err) { 43 | std.Io.Reader.StreamError.EndOfStream => break :blk read_buf.written().len, 44 | else => return err, 45 | } 46 | }; 47 | if (read_len > 0) { 48 | std.debug.print("Request: {s}\n", .{read_buf.written()}); 49 | 50 | // Dispatch the JSON-RPC request to the handler, with result in response JSON. 51 | const run_status = try pipeline.runRequest(read_buf.written()); 52 | if (run_status.hasReply()) { 53 | std.debug.print("Response: {s}\n", .{pipeline.responseJson()}); 54 | } else { 55 | std.debug.print("No response\n", .{}); 56 | } 57 | } else { 58 | usage(); 59 | } 60 | } 61 | 62 | 63 | fn hello() []const u8 { 64 | return "Hello world"; 65 | } 66 | 67 | fn helloName(dc: *zigjr.DispatchCtx, name: [] const u8) ![]const u8 { 68 | return try std.fmt.allocPrint(dc.arena(), "Hello {s}", .{name}); 69 | } 70 | 71 | fn helloXTimes(dc: *zigjr.DispatchCtx, name: [] const u8, times: i64) ![]const u8 { 72 | const repeat: usize = if (0 < times and times < 100) @intCast(times) else 1; 73 | var buf = std.Io.Writer.Allocating.init(dc.arena()); 74 | var writer = &buf.writer; 75 | for (0..repeat) |_| try writer.print("Hello {s}! ", .{name}); 76 | return buf.written(); 77 | } 78 | 79 | fn say(msg: [] const u8) void { 80 | std.debug.print("Message to say: {s}\n", .{msg}); 81 | } 82 | 83 | 84 | fn usage() void { 85 | std.debug.print( 86 | \\Usage: hello_single 87 | \\Usage: hello_single < message.json 88 | \\ 89 | \\The program reads from stdin. 90 | , .{}); 91 | } 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /examples/hello.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const std = @import("std"); 10 | const Allocator = std.mem.Allocator; 11 | 12 | const zigjr = @import("zigjr"); 13 | 14 | 15 | pub fn main() !void { 16 | var gpa = std.heap.DebugAllocator(.{}){}; 17 | defer _ = gpa.deinit(); 18 | const alloc = gpa.allocator(); 19 | 20 | // Create a RpcDispatcher for the JSON-RPC handlers. 21 | var rpc_dispatcher = try zigjr.RpcDispatcher.init(alloc); 22 | defer rpc_dispatcher.deinit(); 23 | 24 | // Register each RPC method with a handling function. 25 | try rpc_dispatcher.add("hello", hello); 26 | try rpc_dispatcher.add("hello-name", helloName); 27 | try rpc_dispatcher.add("hello-xtimes", helloXTimes); 28 | try rpc_dispatcher.add("substr", substr); 29 | try rpc_dispatcher.add("say", say); 30 | try rpc_dispatcher.add("opt-text", optionalText); 31 | 32 | var stdin_buffer: [1024]u8 = undefined; 33 | var stdin_reader = std.fs.File.stdin().reader(&stdin_buffer); 34 | const stdin = &stdin_reader.interface; 35 | 36 | var stdout_buffer: [1024]u8 = undefined; 37 | var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); 38 | const stdout = &stdout_writer.interface; 39 | 40 | // Read requests from stdin, dispatch to handlers, and write responses to stdout. 41 | const dispatcher = zigjr.RequestDispatcher.implBy(&rpc_dispatcher); 42 | try zigjr.stream.requestsByDelimiter(alloc, stdin, stdout, dispatcher, .{}); 43 | try stdout.flush(); 44 | } 45 | 46 | 47 | // A handler with no parameter and returns a string. 48 | fn hello() []const u8 { 49 | return "Hello world"; 50 | } 51 | 52 | // A handler takes in a string parameter and returns a string with error. 53 | // It also asks the library for an allocator, which is passed in automatically. 54 | // Allocated memory is freed automatically, making memory usage simple. 55 | fn helloName(dc: *zigjr.DispatchCtx, name: [] const u8) Allocator.Error![]const u8 { 56 | return try std.fmt.allocPrint(dc.arena(), "Hello {s}", .{name}); 57 | } 58 | 59 | // This one takes one more parameter. Note that i64 is JSON's integer type. 60 | fn helloXTimes(dc: *zigjr.DispatchCtx, name: [] const u8, times: i64) ![]const u8 { 61 | const repeat: usize = if (0 < times and times < 100) @intCast(times) else 1; 62 | var buf = std.Io.Writer.Allocating.init(dc.arena()); 63 | for (0..repeat) |_| try buf.writer.print("Hello {s}! ", .{name}); 64 | return buf.written(); 65 | } 66 | 67 | fn substr(name: [] const u8, start: i64, len: i64) []const u8 { 68 | return name[@intCast(start) .. @intCast(len)]; 69 | } 70 | 71 | // A handler takes in a string and has no return value, for RPC notification. 72 | fn say(msg: [] const u8) void { 73 | std.debug.print("Message to say: {s}\n", .{msg}); 74 | } 75 | 76 | fn optionalText(text: ?[] const u8) []const u8 { 77 | if (text)|txt| { 78 | return txt; 79 | } else { 80 | return "No text"; 81 | } 82 | } 83 | 84 | 85 | fn usage() void { 86 | std.debug.print( 87 | \\Usage: hello 88 | \\Usage: hello < messages.json 89 | \\ 90 | \\The program reads from stdin. 91 | , .{}); 92 | } 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /src/zigjr.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const request = @import("jsonrpc/request.zig"); 10 | const response = @import("jsonrpc/response.zig"); 11 | const message = @import("jsonrpc/message.zig"); 12 | pub const errors = @import("jsonrpc/errors.zig"); 13 | pub const composer = @import("jsonrpc/composer.zig"); 14 | pub const pipeline = @import("rpc/rpc_pipeline.zig"); 15 | const dispatcher = @import("rpc/dispatcher.zig"); 16 | const rpc_dispatcher = @import("rpc/rpc_dispatcher.zig"); 17 | pub const stream = @import("streaming/stream.zig"); 18 | pub const frame = @import("streaming/frame.zig"); 19 | const logger = @import("rpc/logger.zig"); 20 | pub const json_call = @import("rpc/json_call.zig"); 21 | 22 | pub const parseRpcRequest = request.parseRpcRequest; 23 | pub const parseRpcRequestOwned = request.parseRpcRequestOwned; 24 | pub const RpcRequestResult = request.RpcRequestResult; 25 | pub const RpcRequestMessage = request.RpcRequestMessage; 26 | pub const RpcRequest = request.RpcRequest; 27 | pub const RpcId = request.RpcId; 28 | pub const RpcRequestError = request.RpcRequestError; 29 | 30 | pub const parseRpcResponse = response.parseRpcResponse; 31 | pub const parseRpcResponseOwned = response.parseRpcResponseOwned; 32 | pub const RpcResponseResult = response.RpcResponseResult; 33 | pub const RpcResponseMessage = response.RpcResponseMessage; 34 | pub const RpcResponse = response.RpcResponse; 35 | pub const RpcResponseError = response.RpcResponseError; 36 | 37 | pub const parseRpcMessage = message.parseRpcMessage; 38 | 39 | pub const RequestDispatcher = dispatcher.RequestDispatcher; 40 | pub const ResponseDispatcher = dispatcher.ResponseDispatcher; 41 | pub const DispatchResult = dispatcher.DispatchResult; 42 | pub const DispatchErrors = dispatcher.DispatchErrors; 43 | pub const DispatchCtxImpl = dispatcher.DispatchCtxImpl; 44 | pub const DispatchCtx = json_call.DispatchCtx; 45 | 46 | pub const RequestPipeline = pipeline.RequestPipeline; 47 | pub const ResponsePipeline = pipeline.ResponsePipeline; 48 | pub const MessagePipeline = pipeline.MessagePipeline; 49 | pub const RunStatus = pipeline.RunStatus; 50 | 51 | pub const Logger = logger.Logger; 52 | pub const NopLogger = logger.NopLogger; 53 | pub const DbgLogger = logger.DbgLogger; 54 | pub const FileLogger = logger.FileLogger; 55 | 56 | pub const RpcDispatcher = rpc_dispatcher.RpcDispatcher; 57 | pub const RegistrationErrors = rpc_dispatcher.RegistrationErrors; 58 | pub const H_PRE_REQUEST = rpc_dispatcher.H_PRE_REQUEST; 59 | pub const H_FALLBACK = rpc_dispatcher.H_FALLBACK; 60 | pub const H_END_REQUEST = rpc_dispatcher.H_END_REQUEST; 61 | pub const H_ON_ERROR = rpc_dispatcher.H_ON_ERROR; 62 | 63 | pub const JsonStr = @import("rpc/json_call.zig").JsonStr; 64 | pub const ErrorCode = errors.ErrorCode; 65 | pub const JrErrors = errors.JrErrors; 66 | 67 | 68 | test { 69 | _ = @import("tests/request_tests.zig"); 70 | _ = @import("tests/response_tests.zig"); 71 | _ = @import("tests/message_tests.zig"); 72 | _ = @import("tests/frame_tests.zig"); 73 | _ = @import("tests/stream_tests.zig"); 74 | _ = @import("tests/rpc_dispatcher_tests.zig"); 75 | _ = @import("tests/json_call_tests.zig"); 76 | _ = @import("tests/misc_tests.zig"); 77 | _ = @import("streaming/BufReader.zig"); 78 | _ = @import("streaming/DupWriter.zig"); 79 | } 80 | 81 | 82 | -------------------------------------------------------------------------------- /examples/dispatcher_counter.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const std = @import("std"); 10 | const Allocator = std.mem.Allocator; 11 | 12 | const zigjr = @import("zigjr"); 13 | const RpcRequest = zigjr.RpcRequest; 14 | const DispatchResult = zigjr.DispatchResult; 15 | const ErrorCode = zigjr.ErrorCode; 16 | 17 | 18 | pub fn main() !void { 19 | var gpa = std.heap.DebugAllocator(.{}){}; 20 | defer _ = gpa.deinit(); 21 | const alloc = gpa.allocator(); 22 | 23 | var stdin_buffer: [1024]u8 = undefined; 24 | var stdin_reader = std.fs.File.stdin().reader(&stdin_buffer); 25 | const stdin = &stdin_reader.interface; 26 | 27 | var stdout_buffer: [1024]u8 = undefined; 28 | var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); 29 | const stdout = &stdout_writer.interface; 30 | 31 | // RequestDispatcher interface implemented by the custom dispatcher. 32 | var counter_dispatcher = CounterDispatcher {}; 33 | const dispatcher = zigjr.RequestDispatcher.implBy(&counter_dispatcher); 34 | // try zigjr.stream.requestsByDelimiter(alloc, stdin, stdout, dispatcher, .{}); 35 | 36 | // Construct a pipeline with the custom dispatcher. 37 | var pipeline = try zigjr.RequestPipeline.init(alloc, dispatcher, null); 38 | defer pipeline.deinit(); 39 | 40 | var read_buf = std.Io.Writer.Allocating.init(alloc); 41 | defer read_buf.deinit(); 42 | 43 | while (true) { 44 | // Read request from stdin. 45 | _ = stdin.streamDelimiter(&read_buf.writer, '\n') catch |e| { 46 | switch (e) { 47 | error.EndOfStream => break, 48 | else => return e, // unrecoverable error while reading from reader. 49 | } 50 | }; 51 | stdin.toss(1); 52 | 53 | try stdout.print("Request: {s}\n", .{read_buf.written()}); 54 | 55 | // Run request through the pipeline. 56 | const run_status = try pipeline.runRequest(read_buf.written()); 57 | if (run_status.hasReply()) { 58 | try stdout.print("Response: {s}\n", .{pipeline.responseJson()}); 59 | } else { 60 | try stdout.print("No response\n", .{}); 61 | } 62 | try stdout.flush(); 63 | 64 | read_buf.clearRetainingCapacity(); 65 | } 66 | } 67 | 68 | 69 | const CounterDispatcher = struct { 70 | count: isize = 1, // start with 1. 71 | 72 | pub fn dispatch(self: *@This(), dc: *zigjr.DispatchCtxImpl) !DispatchResult { 73 | if (std.mem.eql(u8, dc.request.method, "inc")) { 74 | self.count += 1; 75 | return DispatchResult.asNone(); // treat request as notification 76 | } else if (std.mem.eql(u8, dc.request.method, "dec")) { 77 | self.count -= 1; 78 | return DispatchResult.asNone(); // treat request as notification 79 | } else if (std.mem.eql(u8, dc.request.method, "get")) { 80 | return DispatchResult.withResult(try std.json.Stringify.valueAlloc(dc.arena, self.count, .{})); 81 | } else { 82 | return DispatchResult.withErr(ErrorCode.MethodNotFound, ""); 83 | } 84 | } 85 | 86 | pub fn dispatchEnd(_: *@This(), dc: *zigjr.DispatchCtxImpl) void { 87 | _=dc; 88 | } 89 | }; 90 | 91 | 92 | fn usage() void { 93 | std.debug.print( 94 | \\Usage: dispatcher_counter 95 | \\Usage: dispatcher_counter < message.json 96 | \\ 97 | \\The program reads from stdin. 98 | , .{}); 99 | } 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/tests/frame_tests.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Type = std.builtin.Type; 3 | const testing = std.testing; 4 | const allocPrint = std.fmt.allocPrint; 5 | const Allocator = std.mem.Allocator; 6 | const ArrayList = std.ArrayList; 7 | const nanoTimestamp = std.time.nanoTimestamp; 8 | const Value = std.json.Value; 9 | const Array = std.json.Array; 10 | const frame = @import("../streaming/frame.zig"); 11 | 12 | 13 | test "readHttpHeaders" { 14 | var gpa = std.heap.DebugAllocator(.{}){}; 15 | defer _ = gpa.deinit(); 16 | const alloc = gpa.allocator(); 17 | 18 | { 19 | var frame_data = frame.FrameData.init(alloc); 20 | defer frame_data.deinit(); 21 | const frame_text = 22 | \\Content-Length: 30 23 | \\Header1: abc 24 | \\ Header2: Xyz 25 | ++ "\r\n\r\n" ++ 26 | \\content-data 27 | \\more content-data 28 | ; 29 | // std.debug.print("frame_text: {s}\n", .{frame_text}); 30 | var input_reader = std.Io.Reader.fixed(frame_text); 31 | try frame.readHttpHeaders(&input_reader, &frame_data); 32 | 33 | // for (0..frame_data.headerCount())|idx| { 34 | // const key = frame_data.headerKey(idx); 35 | // const value = frame_data.headerValue(idx); 36 | // std.debug.print("key: '{s}', value: '{s}'\n", .{ key, value }); 37 | // } 38 | try testing.expectEqualStrings(frame_data.findHeader("Content-Length").?, "30"); 39 | try testing.expectEqualStrings(frame_data.findHeader("Header1").?, "abc"); 40 | try testing.expectEqualStrings(frame_data.findHeader("Header2").?, "Xyz"); 41 | try testing.expect(frame_data.content_length == 30); 42 | 43 | var input_reader2 = std.Io.Reader.fixed(frame_text); 44 | frame_data.reset(); 45 | const has_more1 = try frame.readContentLengthFrame(&input_reader2, &frame_data); 46 | try testing.expect(has_more1); 47 | // std.debug.print("content: |{s}|\n", .{frame_data.getContent()}); 48 | try testing.expectEqualStrings(frame_data.getContent(), 49 | \\content-data 50 | \\more content-data 51 | ); 52 | // frame_data.reset(); 53 | // const has_more2 = try frame.readContentLengthFrame(&input_reader2, &frame_data); 54 | // try testing.expect(!has_more2); 55 | // std.debug.print("content: |{s}|\n", .{frame_data.getContent()}); 56 | } 57 | 58 | 59 | } 60 | 61 | test "writeContentLengthFrame" { 62 | var gpa = std.heap.DebugAllocator(.{}){}; 63 | defer _ = gpa.deinit(); 64 | const alloc = gpa.allocator(); 65 | 66 | { 67 | var w = std.Io.Writer.Allocating.init(alloc); 68 | defer w.deinit(); 69 | try frame.writeContentLengthFrame(&w.writer, "abc"); 70 | try testing.expectEqualStrings(w.written(), 71 | "Content-Length: 3\r\n\r\nabc"); 72 | } 73 | 74 | 75 | } 76 | 77 | test "writeContentLengthFrames" { 78 | var gpa = std.heap.DebugAllocator(.{}){}; 79 | defer _ = gpa.deinit(); 80 | const alloc = gpa.allocator(); 81 | 82 | { 83 | const data = [_][]const u8{ 84 | \\abc 85 | , 86 | \\efgh 87 | , 88 | \\ijk 89 | }; 90 | 91 | var w = std.Io.Writer.Allocating.init(alloc); 92 | defer w.deinit(); 93 | try frame.writeContentLengthFrames(&w.writer, &data); 94 | try testing.expectEqualStrings(w.written(), 95 | "Content-Length: 3\r\n\r\nabc" ++ 96 | "Content-Length: 4\r\n\r\nefgh" ++ 97 | "Content-Length: 3\r\n\r\nijk" 98 | ); 99 | } 100 | 101 | 102 | } 103 | 104 | -------------------------------------------------------------------------------- /examples/dispatcher_hello.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const std = @import("std"); 10 | const Allocator = std.mem.Allocator; 11 | 12 | const zigjr = @import("zigjr"); 13 | const RpcRequest = zigjr.RpcRequest; 14 | const DispatchResult = zigjr.DispatchResult; 15 | const ErrorCode = zigjr.ErrorCode; 16 | 17 | 18 | pub fn main() !void { 19 | var gpa = std.heap.DebugAllocator(.{}){}; 20 | defer _ = gpa.deinit(); 21 | const alloc = gpa.allocator(); 22 | 23 | var stdin_buffer: [1024]u8 = undefined; 24 | var stdin_reader = std.fs.File.stdin().reader(&stdin_buffer); 25 | const stdin = &stdin_reader.interface; 26 | 27 | var stdout_buffer: [1024]u8 = undefined; 28 | var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); 29 | const stdout = &stdout_writer.interface; 30 | 31 | // RequestDispatcher interface implemented by the custom dispatcher HelloDispatcher. 32 | var hello_dispatcher = HelloDispatcher {}; 33 | const dispatcher = zigjr.RequestDispatcher.implBy(&hello_dispatcher); 34 | 35 | // Construct a pipeline with the custom dispatcher. 36 | var pipeline = try zigjr.RequestPipeline.init(alloc, dispatcher, null); 37 | defer pipeline.deinit(); 38 | 39 | // Read request from stdin. 40 | var read_buf = std.Io.Writer.Allocating.init(alloc); 41 | defer read_buf.deinit(); 42 | const read_len = stdin.streamDelimiter(&read_buf.writer, '\n') catch |err| blk: { 43 | switch (err) { 44 | std.Io.Reader.StreamError.EndOfStream => break :blk read_buf.written().len, 45 | else => return err, 46 | } 47 | }; 48 | 49 | if (read_len > 0) { 50 | try stdout.print("Request: {s}\n", .{read_buf.written()}); 51 | 52 | // Run request through the pipeline. 53 | const run_status = try pipeline.runRequest(read_buf.written()); 54 | if (run_status.hasReply()) { 55 | try stdout.print("Response: {s}\n", .{pipeline.responseJson()}); 56 | } else { 57 | try stdout.print("No response\n", .{}); 58 | } 59 | try stdout.flush(); 60 | } else { 61 | usage(); 62 | } 63 | 64 | } 65 | 66 | 67 | const HelloDispatcher = struct { 68 | // The JSON-RPC request has been parsed into a RpcRequest. Dispatch on it here. 69 | pub fn dispatch(_: @This(), dc: *zigjr.DispatchCtxImpl) !DispatchResult { 70 | if (std.mem.eql(u8, dc.request.method, "hello")) { 71 | // Result needs to be in JSON. Memory allocated from arena will be freed automatically. 72 | const resultJson = try std.json.Stringify.valueAlloc(dc.arena, "Hello World", .{}); 73 | return DispatchResult.withResult(resultJson); 74 | } else if (std.mem.eql(u8, dc.request.method, "hello-name")) { 75 | if (dc.request.params == .array) { 76 | const items = dc.request.params.array.items; 77 | if (items.len > 0 and items[0] == .string) { 78 | const result = try std.fmt.allocPrint(dc.arena, "Hello {s}", .{ items[0].string }); 79 | const resultJson = try std.json.Stringify.valueAlloc(dc.arena, result, .{}); 80 | return DispatchResult.withResult(resultJson); 81 | } 82 | } 83 | return DispatchResult.withErr(ErrorCode.InvalidParams, "Invalid params."); 84 | } else if (std.mem.eql(u8, dc.request.method, "hello-xtimes")) { 85 | if (dc.request.params == .array) { 86 | const items = dc.request.params.array.items; 87 | if (items.len > 1 and items[0] == .string and items[1] == .integer) { 88 | const result = try std.fmt.allocPrint(dc.arena, "Hello {s} X {} times", 89 | .{ items[0].string, items[1].integer }); 90 | const resultJson = try std.json.Stringify.valueAlloc(dc.arena, result, .{}); 91 | return DispatchResult.withResult(resultJson); 92 | } 93 | } 94 | return DispatchResult.withErr(ErrorCode.InvalidParams, "Invalid params."); 95 | } else if (std.mem.eql(u8, dc.request.method, "say")) { 96 | if (dc.request.params == .array) { 97 | const items = dc.request.params.array.items; 98 | if (items.len > 0 and items[0] == .string) { 99 | std.debug.print("Say: {s}\n", .{items[0].string}); 100 | } 101 | } 102 | return DispatchResult.asNone(); 103 | } else { 104 | return DispatchResult.withErr(ErrorCode.MethodNotFound, "Method not found."); 105 | } 106 | } 107 | 108 | // The result has been processed; this call is the chance to clean up. 109 | // After this call, the request and result in dc will be reset. 110 | pub fn dispatchEnd(_: @This(), dc: *zigjr.DispatchCtxImpl) void { 111 | _=dc; 112 | } 113 | }; 114 | 115 | 116 | fn usage() void { 117 | std.debug.print( 118 | \\Usage: dispatcher_hello 119 | \\Usage: dispatcher_hello < message.json 120 | \\ 121 | \\The program reads from stdin. 122 | , .{}); 123 | } 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /todo-zigjr.org: -------------------------------------------------------------------------------- 1 | 2 | * TODOs 3 | ** DONE Add dispatcher.free() to free on the DispatchResult. Let the dispatcher handle freeing memory. 4 | ** DONE Add inc(), dec(), and get() to CounterDispatcher test. 5 | ** DONE Add notification testing. Just use CounterDispatcher. 6 | ** DONE Test on float number in request and response messages. 7 | ** DONE Add check for notification request. 8 | ** DONE Add batch response message builder. 9 | ** DONE Add batch dispatching in msg_handler. 10 | ** DONE Add parsing batch result. 11 | ** DONE Rename RpcMessage to RpcRequestMessage. 12 | ** DONE Add test on parsing empty batch request. 13 | ** DONE Add test on parsing empty batch response. 14 | ** DONE Add test on parsing batch requests with reader. 15 | ** DONE Add delimiter based request streaming API. 16 | ** DONE Add Content-Length request header based streaming API. 17 | ** DONE Add running dispatcher on response JSON, on RpcResponse, and on RpcResponse batch. 18 | ** DONE Add delimiter based response streaming API. 19 | ** DONE Add Content-Length header based response streaming API. 20 | ** DONE Message logging. 21 | ** DONE Rename run() to dispatch() on dispatcher. 22 | ** DONE Compact JSON output. 23 | ** DONE Return DispatchResult for the Callable.invocation. 24 | ** DONE Registration of function with arbitrary parameters. 25 | ** DONE Invoke function with arbitrary parameters. 26 | ** DONE Registration of function with arbitrary return type. 27 | ** DONE Capture the function return value and convert to JSON result. 28 | ** DONE Convert JSON to native values. 29 | ** DONE Use ArenaAllocator for each request handling invocation to avoid complicated cleanup. 30 | ** DONE Add dispatching registry. 31 | ** DONE Rename directory handler to rpc. 32 | ** DONE Allow struct objects in array params. 33 | ** DONE Add Hello example for a simple start. 34 | ** DONE Add Calc example for various function handlers, parameter types, and return types. 35 | ** DONE Add streaming example. 36 | ** DONE Support handling arbitrary Value params in the array parameters of a handler function. 37 | ** DONE Add LSP client example. 38 | ** DONE Add fallback handler for RpcRegistry. 39 | ** DONE Add pre-dispatch and post-dispatch handlers for RpcRegistry. 40 | ** DONE 0.15.1: Convert reader: anytype to reader: Io.Reader 41 | ** DONE 0.15.1: Convert writer: anytype to writer: Io.Writer 42 | ** DONE 0.15.1: Convert deprecated ArrayList.writer() to std.io.Writer.Allocating. 43 | ** DONE 0.15.1: migrate std.io.bufferedWriter to new Io.Writer. 44 | ** DONE 0.15.1: get float from std.json.Value.integer/float. 45 | ** DONE Add support for single optional parameter in json_call. 46 | ** DONE Add support for single optional parameter in rpc_pipeline. 47 | ** DONE Support optional "params" in JSON RPC 2.0. 48 | ** DONE Add network server example. 49 | ** DONE Migrate allocator in RpcHandler to caller, to allow for per-thread allocator instead of global allocator in the RpcHandler. 50 | ** DONE Add DispatchCtxImpl with user type support to handler. 51 | ** TODO Have a separate DispatchCtxImpl for RpcDispatcher/Dispatcher and a separate DispatchCtx(R) for RpcHandler. 52 | ** TODO Add per-request user data type to DispatchCtxImpl and RpcHandler. 53 | ** TODO Per-request dispatch context. 54 | *** TODO Per-request dispatch context setup on onBefore. 55 | *** TODO Per-request dispatch context cleanup on onEnd. 56 | ** TODO Add session store. 57 | ** TODO Add request and notification message builders. 58 | 59 | 60 | * Releases 61 | 62 | - Releases 1.9.0 63 | * Add the .asLogger() convenient method in DbgLogger and FileLogger for ease of usage. 64 | * Add frame.readHttpLine() to parse the HTTP request line. 65 | 66 | - Releases 1.8.0 67 | * Make the example hello_net.zig return a proper HTTP response in the --http mode. 68 | * Allow setting listening port in hello_net server example. 69 | * Handling server termination command as a JSON-RPC command. 70 | 71 | - Releases 1.7.0 72 | * Add a network based JSON-RPC server example, over either HTTP or plain TCP. 73 | * Use more rich RunStatus instead of bool for return value of runXX(). 74 | Allow stream to be canceled via the RunStatus.end_stream flag. 75 | * Flush writer at the end of writing a Content-Length based message. 76 | 77 | - Releases 1.6.0 78 | * Breaking change. FileLogger allocates its own write_buf now instead of taking in a passed in buffer, making it easier to use and more self contained. 79 | 80 | - Releases 1.5.0 81 | * Breaking change. Simplify RpcPipeline.runRequest() API by removing of the passed in Allocator. 82 | * Breaking change. Simplify dispatcher API. Remove the need to pass in an Allocator to dispatch() and dispatchEnd(). The Dispatcher object is expected to manage its own allocator as needed. 83 | * Redo and formalize extended handlers on RpcDispatcher. Add OnErrorHandler. Add tests. 84 | 85 | - Releases 1.4.0 86 | * Breaking change! 87 | Rename RpcRegistry to RpcDispatcher. Rename rpc_registry.zig to rpc_dispatcher.zig. Add stream.runByDelimiter() and stream.runByContentLength() to take on RpcDispatcher as parameter. 88 | 89 | - Releases 1.3.0 90 | * Breaking change! Migrated to use Zig 0.15.1 API. 91 | 92 | - Release 1.1 93 | * Add Universal message handling. 94 | * Message-based parsing, for both request and response. 95 | * Message-based execution via rpc_pipeline.runMessage(), for both request and response. 96 | * Message-based streaming, handling both request and response in the stream. 97 | * RpcRegistry supports extended handlers: pre-dispatch, post-dispatch, and fallback handler. 98 | * Fallback handler for handling any unregistered request methods. 99 | * Add RpcRequest.parseRpcRequestOwned() to handle memory ownership of passed in JSON string. 100 | * Add RpcResponse.parseRpcResponseOwned() to handle memory ownership of passed in JSON string. 101 | * Remove error union from the return value of response.parseRpcResponse(). 102 | * Add readHttpHeaders() to parse all HTTP-style headers, not just Content-Length. 103 | * Add the LSP client example. 104 | 105 | - Release 1.0.1 106 | * Minor bug fixes. 107 | 108 | - Release 1.0 109 | * Feature completed. Initial release. 110 | 111 | -------------------------------------------------------------------------------- /src/tests/misc_tests.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Type = std.builtin.Type; 3 | const testing = std.testing; 4 | const allocPrint = std.fmt.allocPrint; 5 | const Allocator = std.mem.Allocator; 6 | const ArrayList = std.ArrayList; 7 | const nanoTimestamp = std.time.nanoTimestamp; 8 | const Value = std.json.Value; 9 | const Array = std.json.Array; 10 | 11 | const Deiniter = @import("../rpc/deiniter.zig").Deiniter; 12 | const ConstDeiniter = @import("../rpc/deiniter.zig").ConstDeiniter; 13 | 14 | 15 | fn foo(a: u8, b: i32) void { std.debug.print("foo: a={}, b={}\n", .{a, b}); } 16 | fn bar(a: f64) void { std.debug.print("bar: a={}\n", .{a}); } 17 | 18 | fn ofFn(comptime func: anytype) void { 19 | const fn_info = @typeInfo(@TypeOf(func)).@"fn"; 20 | //std.debug.print("fn_info={any}\n", .{fn_info.params}); 21 | inline for (fn_info.params, 0..)|param, i| { 22 | std.debug.print("arg_{d}: param={any}\n", .{i, param}); 23 | // std.fmt.comptimePrint("arg_{d}: type={any}", .{i, param.type}); 24 | } 25 | } 26 | 27 | fn paramsAsTuple(comptime func: anytype) type { 28 | const fn_info = @typeInfo(@TypeOf(func)).@"fn"; 29 | 30 | comptime var fields: [fn_info.params.len]std.builtin.Type.StructField = undefined; 31 | inline for (fn_info.params, 0..)|param, i| { 32 | fields[i] = .{ 33 | .name = std.fmt.comptimePrint("{d}", .{i}), 34 | .type = param.type orelse null, 35 | .default_value_ptr = null, 36 | .is_comptime = false, 37 | .alignment = 0, 38 | }; 39 | } 40 | 41 | return @Type(.{ 42 | .@"struct" = .{ 43 | .layout = .auto, 44 | .fields = fields[0..], 45 | .decls = &.{}, 46 | .is_tuple = true, 47 | }, 48 | }); 49 | } 50 | 51 | fn makeTuple(comptime tuple_type: type) tuple_type { 52 | const tt_info = @typeInfo(tuple_type).@"struct"; 53 | var tuple: tuple_type = undefined; 54 | inline for (tt_info.fields, 0..)|field, i| { 55 | std.debug.print("@\"{d}\": {any}\n", .{i, field}); 56 | @field(tuple, field.name) = 42; 57 | } 58 | return tuple; 59 | } 60 | 61 | fn jsonToTuple(comptime tuple_type: type, args: Array) tuple_type { 62 | const tt_info = @typeInfo(tuple_type).@"struct"; 63 | var tuple: tuple_type = undefined; 64 | inline for (tt_info.fields, 0..)|field, i| { 65 | const arg = args.items[i]; 66 | std.debug.print("@\"{d}\"| field: {any} | arg: {any}\n", .{i, field, arg}); 67 | @field(tuple, field.name) = 42; 68 | switch (field.type) { 69 | bool => { 70 | @field(tuple, field.name) = arg.bool; 71 | }, 72 | u8 => { 73 | @field(tuple, field.name) = @as(u8, @intCast(arg.integer)); 74 | }, 75 | i32 => { 76 | @field(tuple, field.name) = @as(i32, @intCast(arg.integer)); 77 | }, 78 | f64 => { 79 | @field(tuple, field.name) = arg.float; 80 | }, 81 | else => {} 82 | } 83 | } 84 | return tuple; 85 | } 86 | 87 | test "Misc" { 88 | var gpa = std.heap.DebugAllocator(.{}){}; 89 | defer _ = gpa.deinit(); 90 | const alloc = gpa.allocator(); 91 | 92 | { 93 | var s1 = struct { 94 | a: u8 = 'A', 95 | b: i32 = 1, 96 | }{}; 97 | std.debug.print("s1={any}\n", .{s1}); 98 | @field(s1, "b") = 10; 99 | std.debug.print("s1={any}\n", .{s1}); 100 | 101 | const t1 = .{ 'A', 1 }; 102 | std.debug.print("t1={any}\n", .{t1}); 103 | 104 | foo('A', 2); 105 | bar(1.1); 106 | 107 | ofFn(foo); 108 | ofFn(bar); 109 | 110 | const foo_type1 = paramsAsTuple(foo); 111 | std.debug.print("foo_type1={any}\n", .{foo_type1}); 112 | const foo_value1: foo_type1 = .{ 'B', 2 }; 113 | std.debug.print("foo_value1={any}\n", .{foo_value1}); 114 | 115 | const bar_type1 = paramsAsTuple(bar); 116 | std.debug.print("bar_type1={any}\n", .{bar_type1}); 117 | var bar_value1: bar_type1 = .{ 2.2 }; 118 | std.debug.print("bar_value1={any}\n", .{bar_value1}); 119 | bar_value1.@"0" = 3.3; 120 | std.debug.print("bar_value1={any}\n", .{bar_value1}); 121 | 122 | const tt1 = makeTuple(foo_type1); 123 | std.debug.print("tt1={any}\n", .{tt1}); 124 | 125 | var args1 = Array.init(alloc); 126 | defer args1.deinit(); 127 | try args1.append(.{ .integer = 4 }); 128 | try args1.append(.{ .integer = 40 }); 129 | const tt2 = jsonToTuple(foo_type1, args1); 130 | std.debug.print("tt2={any}\n", .{tt2}); 131 | 132 | var bar_args2 = Array.init(alloc); 133 | defer bar_args2.deinit(); 134 | try bar_args2.append(.{ .float = 4.4 }); 135 | const bb1 = jsonToTuple(bar_type1, bar_args2); 136 | std.debug.print("bb1={any}\n", .{bb1}); 137 | 138 | } 139 | 140 | 141 | } 142 | 143 | test "Deiniter var struct" { 144 | var gpa = std.heap.DebugAllocator(.{}){}; 145 | defer _ = gpa.deinit(); 146 | const alloc = gpa.allocator(); 147 | 148 | { 149 | var obj1 = struct { 150 | alloc: Allocator, 151 | text: []const u8, 152 | pub fn deinit(self: @This()) void { 153 | self.alloc.free(self.text); 154 | } 155 | } { 156 | .alloc = alloc, 157 | .text = try Allocator.dupe(alloc, u8, "This is a test"), 158 | }; 159 | 160 | var de1 = Deiniter.implBy(&obj1); 161 | de1.deinit(); 162 | 163 | } 164 | 165 | } 166 | 167 | test "Deiniter const struct" { 168 | var gpa = std.heap.DebugAllocator(.{}){}; 169 | defer _ = gpa.deinit(); 170 | const alloc = gpa.allocator(); 171 | 172 | { 173 | var de1 = ConstDeiniter.implBy(&struct { 174 | alloc: Allocator, 175 | text: []const u8, 176 | pub fn deinit(self: @This()) void { 177 | self.alloc.free(self.text); 178 | } 179 | } { 180 | .alloc = alloc, 181 | .text = try Allocator.dupe(alloc, u8, "This is a test"), 182 | }); 183 | de1.deinit(); 184 | 185 | } 186 | 187 | } 188 | 189 | 190 | -------------------------------------------------------------------------------- /src/rpc/logger.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const std = @import("std"); 10 | const Allocator = std.mem.Allocator; 11 | 12 | 13 | inline fn TPtr(T: type, opaque_ptr: *anyopaque) T { 14 | return @as(T, @ptrCast(@alignCast(opaque_ptr))); 15 | } 16 | 17 | /// Logger interface 18 | pub const Logger = struct { 19 | impl: *anyopaque, 20 | i_start: *const fn(impl: *anyopaque, message: []const u8) void, 21 | i_log: *const fn(impl: *anyopaque, source: [] const u8, operation: []const u8, message: []const u8) void, 22 | i_stop: *const fn(impl: *anyopaque, message: []const u8) void, 23 | 24 | // The implementation must have these methods. 25 | pub fn start(self: @This(), message: []const u8) void { 26 | self.i_start(self.impl, message); 27 | } 28 | 29 | pub fn log(self: @This(), source: [] const u8, operation: []const u8, message: []const u8) void { 30 | self.i_log(self.impl, source, operation, message); 31 | } 32 | 33 | pub fn stop(self: @This(), message: []const u8) void { 34 | self.i_stop(self.impl, message); 35 | } 36 | 37 | // Interface is implemented by the 'impl_obj' object. 38 | pub fn implBy(impl_obj: anytype) Logger { 39 | const IT = @TypeOf(impl_obj); 40 | 41 | const Delegate = struct { 42 | fn start(impl: *anyopaque, message: []const u8) void { 43 | TPtr(IT, impl).start(message); 44 | } 45 | 46 | fn log(impl: *anyopaque, source: [] const u8, operation: []const u8, message: []const u8) void { 47 | TPtr(IT, impl).log(source, operation, message); 48 | } 49 | 50 | fn stop(impl: *anyopaque, message: []const u8) void { 51 | TPtr(IT, impl).stop(message); 52 | } 53 | }; 54 | 55 | return .{ 56 | .impl = impl_obj, 57 | .i_start = Delegate.start, 58 | .i_log = Delegate.log, 59 | .i_stop = Delegate.stop, 60 | }; 61 | } 62 | 63 | }; 64 | 65 | 66 | /// A nop logger that implements the Logger interface; can be passed to the stream options.logger. 67 | pub const NopLogger = struct { 68 | pub fn start(_: @This(), _: []const u8) void {} 69 | pub fn log(_: @This(), _: []const u8, _: []const u8, _: []const u8) void {} 70 | pub fn stop(_: @This(), _: []const u8) void {} 71 | 72 | /// Return a Logger interface for this DbgLogger. 73 | pub fn asLogger(self: *NopLogger) Logger { 74 | return Logger.implBy(self); 75 | } 76 | }; 77 | 78 | var nopLogger = NopLogger{}; 79 | 80 | 81 | /// A logger that prints to std.dbg, implemented the Logger interface. 82 | pub const DbgLogger = struct { 83 | pub fn start(_: *DbgLogger, message: []const u8) void { 84 | std.debug.print("{s}\n", .{message}); 85 | } 86 | 87 | pub fn log(_: *DbgLogger, source: []const u8, operation: []const u8, message: []const u8) void { 88 | std.debug.print("[{s}] {s} - {s}\n", .{source, operation, message}); 89 | } 90 | 91 | pub fn stop(_: *DbgLogger, message: []const u8) void { 92 | std.debug.print("{s}\n", .{message}); 93 | } 94 | 95 | /// Return a Logger interface for this DbgLogger. 96 | pub fn asLogger(self: *DbgLogger) Logger { 97 | return Logger.implBy(self); 98 | } 99 | 100 | }; 101 | 102 | 103 | /// A logger that logs to file, implemented the Logger interface. 104 | pub const FileLogger = struct { 105 | alloc: Allocator, 106 | count: usize = 0, 107 | file: std.fs.File, 108 | fwriter: std.fs.File.Writer, 109 | write_buf: []const u8, 110 | 111 | /// Create a FileLogger to log to the file at 'file_path', 112 | pub fn init(alloc: Allocator, file_path: []const u8) !FileLogger { 113 | const file = try fileOpenIf(file_path); 114 | try file.seekFromEnd(0); // seek to end for appending to file. 115 | const write_buf = try alloc.alloc(u8, 4096); // buffer with the usual disk page size. 116 | return .{ 117 | .alloc = alloc, 118 | .file = file, 119 | .fwriter = file.writer(write_buf), 120 | .write_buf = write_buf, 121 | }; 122 | } 123 | 124 | pub fn deinit(self: *FileLogger) void { 125 | self.file.close(); 126 | self.alloc.free(self.write_buf); 127 | } 128 | 129 | pub fn start(self: *FileLogger, message: []const u8) void { 130 | const ts_sec = std.time.timestamp(); 131 | self.fwriter.interface.print("Timestamp {d} - {s}\n", .{ts_sec, message}) 132 | catch |err| std.debug.print("Error while printing in start(). {any}\n", .{err}); 133 | self.fwriter.interface.flush() 134 | catch |err| std.debug.print("Error while flushing in log(). {any}\n", .{err}); 135 | } 136 | 137 | pub fn log(self: *FileLogger, source: []const u8, operation: []const u8, message: []const u8) void { 138 | self.count += 1; 139 | self.fwriter.interface.print("{}: [{s}] {s} - {s}\n", .{self.count, source, operation, message}) 140 | catch |err| std.debug.print("Error while printing in log(). {any}\n", .{err}); 141 | self.fwriter.interface.flush() 142 | catch |err| std.debug.print("Error while flushing in log(). {any}\n", .{err}); 143 | } 144 | 145 | pub fn stop(self: *FileLogger, message: []const u8) void { 146 | const ts_sec = std.time.timestamp(); 147 | self.fwriter.interface.print("Timestamp {d} - {s}\n\n", .{ts_sec, message}) 148 | catch |err| std.debug.print("Error while printing in stop(). {any}\n", .{err}); 149 | self.fwriter.interface.flush() 150 | catch |err| std.debug.print("Error while flushing in stop(). {any}\n", .{err}); 151 | } 152 | 153 | fn fileOpenIf(file_path: []const u8) !std.fs.File { 154 | return std.fs.cwd().openFile(file_path, .{ .mode = .write_only }) catch |err| { 155 | if (err == error.FileNotFound) { 156 | return try std.fs.cwd().createFile(file_path, .{ .read = false }); 157 | } else { 158 | return err; 159 | } 160 | }; 161 | } 162 | 163 | /// Return a Logger interface for this FileLogger. 164 | pub fn asLogger(self: *FileLogger) Logger { 165 | return Logger.implBy(self); 166 | } 167 | 168 | }; 169 | 170 | 171 | -------------------------------------------------------------------------------- /src/streaming/BufReader.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const BufReader = @This(); 10 | 11 | const std = @import("std"); 12 | const Allocator = std.mem.Allocator; 13 | 14 | /// Provides a buffered reader implementing the std.Io.Reader interface. 15 | /// Wraps a source reader with arbitrary buffer size into a reader with your own buffer size. 16 | /// Some std.Io.Reader operations only work against the data in its buffer and might need bigger buffer. 17 | 18 | // The source reader to read data from. 19 | src_reader: std.Io.Reader, 20 | // The std.Io.Reader interface for this reader implementation. 21 | buf_interface: std.Io.Reader, 22 | 23 | 24 | pub fn init(alloc: Allocator, buf_size: usize, src_reader: std.Io.Reader) Allocator.Error!@This() { 25 | return .{ 26 | .src_reader = src_reader, 27 | .buf_interface = .{ 28 | .vtable = &.{ 29 | .stream = streamSource, 30 | }, 31 | .buffer = try alloc.alloc(u8, buf_size), 32 | .seek = 0, 33 | .end = 0, 34 | }, 35 | }; 36 | } 37 | 38 | pub fn deinit(self: *BufReader, alloc: Allocator) void { 39 | alloc.free(self.interface().buffer); 40 | } 41 | 42 | 43 | /// Get the std.Io.Reader interface pointer, to avoid an accidental copy. 44 | pub fn interface(self: *BufReader) *std.Io.Reader { 45 | return &self.buf_interface; 46 | } 47 | 48 | pub fn getBufSize(self: *const BufReader) usize { 49 | return self.buf_interface.buffer.len; 50 | } 51 | 52 | // std.Io.Reader calls self.vtable.stream() fills its buffer. 53 | // Reads the data from source to provide the streaming data. 54 | fn streamSource(io_reader: *std.Io.Reader, w: *std.Io.Writer, 55 | limit: std.Io.Limit) std.Io.Reader.StreamError!usize { 56 | const buf_reader: *BufReader = @alignCast(@fieldParentPtr("buf_interface", io_reader)); 57 | return buf_reader.src_reader.stream(w, limit); 58 | } 59 | 60 | 61 | test "Test buffer size" { 62 | var gpa = std.heap.DebugAllocator(.{}){}; 63 | const alloc = gpa.allocator(); 64 | // buffered size bigger than source buffer size 65 | { 66 | const src_reader: std.Io.Reader = .fixed("abc"); 67 | var buf_reader = try BufReader.init(alloc, 80, src_reader); 68 | defer buf_reader.deinit(alloc); 69 | var br = buf_reader.interface(); 70 | try std.testing.expectEqualStrings("a", try br.peek(1)); 71 | try std.testing.expectEqualStrings("ab", try br.peek(2)); 72 | try std.testing.expectEqualStrings("abc", try br.peek(3)); 73 | var data: [1024]u8 = undefined; 74 | const len = try br.readSliceShort(&data); 75 | try std.testing.expect(len == 3); 76 | try std.testing.expectEqualStrings("abc", data[0..len]); 77 | } 78 | // buffered size equals to source buffer size 79 | { 80 | const src_reader: std.Io.Reader = .fixed("abc"); 81 | var buf_reader = try BufReader.init(alloc, 3, src_reader); 82 | defer buf_reader.deinit(alloc); 83 | var br = buf_reader.interface(); 84 | try std.testing.expectEqualStrings("a", try br.peek(1)); 85 | try std.testing.expectEqualStrings("ab", try br.peek(2)); 86 | try std.testing.expectEqualStrings("abc", try br.peek(3)); 87 | var data: [1024]u8 = undefined; 88 | const len = try br.readSliceShort(&data); 89 | try std.testing.expect(len == 3); 90 | try std.testing.expectEqualStrings("abc", data[0..len]); 91 | } 92 | // buffered size less than source buffer size 93 | { 94 | const src_reader: std.Io.Reader = .fixed("abc"); 95 | var buf_reader = try BufReader.init(alloc, 2, src_reader); 96 | defer buf_reader.deinit(alloc); 97 | var br = buf_reader.interface(); 98 | try std.testing.expectEqualStrings("a", try br.peek(1)); 99 | try std.testing.expectEqualStrings("ab", try br.peek(2)); 100 | var data1: [1]u8 = undefined; 101 | const len = try br.readSliceShort(&data1); 102 | try std.testing.expect(len == 1); 103 | try std.testing.expectEqualStrings("a", data1[0..len]); 104 | 105 | try std.testing.expectEqualStrings("b", try br.peek(1)); 106 | try std.testing.expectEqualStrings("bc", try br.peek(2)); 107 | var data: [1024]u8 = undefined; 108 | const len2 = try br.readSliceShort(&data); 109 | try std.testing.expect(len2 == 2); 110 | try std.testing.expectEqualStrings("bc", data[0..len2]); 111 | } 112 | if (gpa.detectLeaks()) std.debug.print("Memory leak detected!\n", .{}); 113 | } 114 | 115 | test "Test buffered data" { 116 | var gpa = std.heap.DebugAllocator(.{}){}; 117 | const alloc = gpa.allocator(); 118 | // buffered size bigger than source buffer size, with peek() 119 | { 120 | const src_reader: std.Io.Reader = .fixed("abc"); 121 | var buf_reader = try BufReader.init(alloc, 80, src_reader); 122 | defer buf_reader.deinit(alloc); 123 | var br = buf_reader.interface(); 124 | _ = try br.peek(1); 125 | try std.testing.expectEqualStrings("abc", br.buffer[0..3]); 126 | } 127 | // buffered size equals to source buffer size, with peek() 128 | { 129 | const src_reader: std.Io.Reader = .fixed("abc"); 130 | var buf_reader = try BufReader.init(alloc, 3, src_reader); 131 | defer buf_reader.deinit(alloc); 132 | var br = buf_reader.interface(); 133 | _ = try br.peek(1); 134 | try std.testing.expectEqualStrings("abc", br.buffer[0..3]); 135 | } 136 | // buffered size less than source buffer size, with peek() 137 | { 138 | const src_reader: std.Io.Reader = .fixed("abc"); 139 | var buf_reader = try BufReader.init(alloc, 2, src_reader); 140 | defer buf_reader.deinit(alloc); 141 | var br = buf_reader.interface(); 142 | _ = try br.peek(1); 143 | try std.testing.expectEqualStrings("ab", br.buffer[0..2]); 144 | } 145 | // buffered size less than source buffer size, with take() 146 | { 147 | const src_reader: std.Io.Reader = .fixed("abc"); 148 | var buf_reader = try BufReader.init(alloc, 2, src_reader); 149 | defer buf_reader.deinit(alloc); 150 | var br = buf_reader.interface(); 151 | _ = try br.take(1); 152 | try std.testing.expectEqualStrings("ab", br.buffer[0..2]); 153 | } 154 | if (gpa.detectLeaks()) std.debug.print("Memory leak detected!\n", .{}); 155 | } 156 | 157 | 158 | -------------------------------------------------------------------------------- /src/rpc/rpc_dispatcher.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const std = @import("std"); 10 | const assert = std.debug.assert; 11 | const Type = std.builtin.Type; 12 | const Allocator = std.mem.Allocator; 13 | const ArenaAllocator = std.heap.ArenaAllocator; 14 | const StringHashMap = std.hash_map.StringHashMap; 15 | const AutoHashMap = std.hash_map.AutoHashMap; 16 | const allocPrint = std.fmt.allocPrint; 17 | 18 | const request = @import("../jsonrpc/request.zig"); 19 | const RpcRequest = request.RpcRequest; 20 | 21 | const dispatcher = @import("dispatcher.zig"); 22 | const RequestDispatcher = dispatcher.RequestDispatcher; 23 | const DispatchResult = dispatcher.DispatchResult; 24 | const DispatchErrors = dispatcher.DispatchErrors; 25 | const DispatchCtxImpl = dispatcher.DispatchCtxImpl; 26 | 27 | const json_call = @import("json_call.zig"); 28 | const DispatchCtx = json_call.DispatchCtx; 29 | 30 | /// Handler names for hooks on different stages of request handling: 31 | pub const H_PRE_REQUEST = "rpc.pre-request"; // called before a request is handled. 32 | pub const H_FALLBACK = "rpc.fallback"; // called when no handler is found for the request. 33 | pub const H_END_REQUEST = "rpc.end-request"; // called after the result is sent back. 34 | pub const H_ON_ERROR = "rpc.on-error"; // called when handler returns an error. 35 | 36 | /// Maintain a list of handlers to handle the RPC requests. 37 | /// Implements the RequestDispatcher interface. 38 | /// The dispatcher is thread-safe in general once it's set up, as long as 39 | /// the addXX and setXX() methods are not called afterward. 40 | /// P is the type of data struct for the per-request user props. 41 | pub const RpcDispatcher = struct { 42 | const Self = @This(); 43 | 44 | handlers: StringHashMap(json_call.RpcHandler), 45 | 46 | pub fn init(alloc: Allocator) RegistrationErrors!Self { 47 | var self: Self = .{ 48 | .handlers = StringHashMap(json_call.RpcHandler).init(alloc), 49 | }; 50 | try self.add(H_PRE_REQUEST, defaultPreRequest); 51 | try self.add(H_FALLBACK, defaultFallback); 52 | try self.add(H_END_REQUEST, defaultEndRequest); 53 | try self.add(H_ON_ERROR, defaultOnError); 54 | return self; 55 | } 56 | 57 | pub fn deinit(self: *Self) void { 58 | self.handlers.deinit(); 59 | } 60 | 61 | pub fn add(self: *Self, method: []const u8, comptime handler_fn: anytype) RegistrationErrors!void { 62 | return self.addWithCtx(method, null, handler_fn); 63 | } 64 | 65 | pub fn addWithCtx(self: *Self, method: []const u8, context: anytype, 66 | comptime handler_fn: anytype) RegistrationErrors!void { 67 | try validateMethod(method); 68 | 69 | // Free any existing handler of the same method name. 70 | _ = self.handlers.fetchRemove(method); 71 | 72 | var dummy_null_ctx = {}; 73 | const ctx = if (@typeInfo(@TypeOf(context)) == .null) &dummy_null_ctx else context; 74 | const h = json_call.makeRpcHandler(ctx, handler_fn); 75 | try self.handlers.put(method, h); 76 | } 77 | 78 | pub fn has(self: *const Self, method: []const u8) bool { 79 | return self.handlers.getPtr(method) != null; 80 | } 81 | 82 | /// Run a handler on the request and generate a DispatchResult. 83 | /// Return any error during the function call. Caller handles any error. 84 | /// Call free() to free the DispatchResult. 85 | // TODO: remove anyerror. Remove DispatchResult; move it to dc. 86 | pub fn dispatch(self: *const Self, dc: *DispatchCtxImpl) anyerror!DispatchResult { 87 | var dcp: DispatchCtx = .{ .dc_impl = dc }; 88 | 89 | self.callHook(&dcp, H_PRE_REQUEST); 90 | 91 | const result = self.callMethod(&dcp) catch |err| { 92 | // TODO: set err as anyerror in dc? 93 | const result = DispatchResult.withAnyErr(err); 94 | dcp.setResult(result); 95 | self.callHook(&dcp, H_ON_ERROR); 96 | return result; 97 | }; 98 | dcp.setResult(result); 99 | return result; 100 | } 101 | 102 | fn callMethod(self: *const Self, dcp: *DispatchCtx) anyerror!DispatchResult { 103 | return if (self.handlers.getPtr(dcp.request().method)) |h| blk1: { 104 | break :blk1 try h.invoke(dcp, dcp.request().params); 105 | } else blk2: { 106 | if (self.handlers.getPtr(H_FALLBACK)) |h| { 107 | // Call fallback handler as a regular handler, with its returning result and error. 108 | break :blk2 try h.invoke(dcp, .{ .null = {}}); 109 | } 110 | unreachable; 111 | }; 112 | } 113 | 114 | fn callHook(self: *const Self, dcp: *DispatchCtx, method: []const u8) void { 115 | if (self.handlers.getPtr(method)) |h| { 116 | _ = h.invoke(dcp, .{ .null = {} }) catch |e| { 117 | std.debug.print("Pre-request handler {s} cannot return an error, but got error: {any}\n", .{method, e}); 118 | unreachable; 119 | }; 120 | } else { 121 | unreachable; 122 | } 123 | } 124 | 125 | pub fn dispatchEnd(self: *const Self, dc: *DispatchCtxImpl) void { 126 | var dcp: DispatchCtx = .{ .dc_impl = dc }; 127 | self.callHook(&dcp, H_END_REQUEST); 128 | dc.reset(); 129 | // Caller is responsible to reset the arena after this point. 130 | // Caller might be batch-processing several requests before reseting the arena. 131 | } 132 | }; 133 | 134 | fn validateMethod(method: []const u8) RegistrationErrors!void { 135 | if (std.mem.startsWith(u8, method, "rpc.")) { // By the JSON-RPC spec, "rpc." is reserved. 136 | const well_known_hooks = 137 | std.mem.eql(u8, method, H_PRE_REQUEST) or 138 | std.mem.eql(u8, method, H_FALLBACK) or 139 | std.mem.eql(u8, method, H_END_REQUEST) or 140 | std.mem.eql(u8, method, H_ON_ERROR); 141 | if (!well_known_hooks) 142 | return RegistrationErrors.InvalidMethodName; 143 | } 144 | } 145 | 146 | fn defaultPreRequest() void {} 147 | fn defaultEndRequest() void {} 148 | fn defaultOnError() void {} 149 | fn defaultFallback() anyerror!DispatchResult { 150 | return DispatchErrors.MethodNotFound; 151 | } 152 | 153 | 154 | pub const RegistrationErrors = error { 155 | InvalidMethodName, 156 | HandlerNotFunction, 157 | MissingAllocatorParameter, 158 | MissingParameterType, 159 | UnsupportedParameterType, 160 | HandlerInvalidParameter, 161 | HandlerInvalidParameterType, 162 | HandlerTooManyParams, 163 | MismatchedParameterCountsForRawParams, 164 | InvalidParamTypeForRawParams, 165 | FallbackHandlerMustHaveValueParam, 166 | OutOfMemory, 167 | }; 168 | 169 | 170 | -------------------------------------------------------------------------------- /examples/calc.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const std = @import("std"); 10 | const Allocator = std.mem.Allocator; 11 | const StringHashMap = std.hash_map.StringHashMap; 12 | 13 | const zigjr = @import("zigjr"); 14 | 15 | 16 | pub fn main() !void { 17 | var gpa = std.heap.DebugAllocator(.{}){}; 18 | defer _ = gpa.deinit(); 19 | const alloc = gpa.allocator(); 20 | 21 | var stash = Stash.init(alloc); 22 | defer stash.deinit(); 23 | 24 | var rpc_dispatcher = try zigjr.RpcDispatcher.init(alloc); 25 | defer rpc_dispatcher.deinit(); 26 | 27 | try rpc_dispatcher.add("add", Basic.add); // register functions in a struct scope. 28 | try rpc_dispatcher.add("subtract", Basic.subtract); 29 | try rpc_dispatcher.add("multiply", Basic.multiply); 30 | try rpc_dispatcher.add("divide", Basic.divide); 31 | try rpc_dispatcher.add("pow", raiseToPower); // register function from any scope. 32 | try rpc_dispatcher.add("logNum", logNum); // function with no result. 33 | try rpc_dispatcher.addWithCtx("inc", &g_sum, increase); // attach a context to the function. 34 | try rpc_dispatcher.addWithCtx("dec", &g_sum, decrease); // attach the same context to another function. 35 | try rpc_dispatcher.addWithCtx("load", &stash, Stash.load); // handler on a struct object context. 36 | try rpc_dispatcher.addWithCtx("save", &stash, Stash.save); // handler on a struct object context. 37 | try rpc_dispatcher.add("weigh-cat", weighCat); // function with a struct parameter. 38 | try rpc_dispatcher.add("make-cat", makeCat); // function returns a struct parameter. 39 | try rpc_dispatcher.add("clone-cat", cloneCat); // function returns an array. 40 | try rpc_dispatcher.add("desc-cat", descCat); // function returns a tuple. 41 | try rpc_dispatcher.add("add-weight", addWeight); 42 | 43 | const dispatcher = zigjr.RequestDispatcher.implBy(&rpc_dispatcher); 44 | var pipeline = try zigjr.RequestPipeline.init(alloc, dispatcher, null); 45 | defer pipeline.deinit(); 46 | 47 | // Read a JSON-RPC request JSON from StdIn. 48 | var stdin_buffer: [256]u8 = undefined; 49 | var stdin_reader = std.fs.File.stdin().reader(&stdin_buffer); 50 | const stdin = &stdin_reader.interface; 51 | var read_buf = std.Io.Writer.Allocating.init(alloc); 52 | defer read_buf.deinit(); 53 | const read_len = stdin.streamDelimiter(&read_buf.writer, '\n') catch |err| blk: { 54 | switch (err) { 55 | std.Io.Reader.StreamError.EndOfStream => break :blk read_buf.written().len, 56 | else => return err, 57 | } 58 | }; 59 | 60 | if (read_len > 0) { 61 | std.debug.print("Request: {s}\n", .{read_buf.written()}); 62 | 63 | const run_status = try pipeline.runRequest(read_buf.written()); 64 | if (run_status.hasReply()) { 65 | std.debug.print("Response: {s}\n", .{pipeline.responseJson()}); 66 | } else { 67 | std.debug.print("No response\n", .{}); 68 | } 69 | } else { 70 | usage(); 71 | } 72 | } 73 | 74 | 75 | const Basic = struct { 76 | fn add(a: i64, b: i64) i64 { return a + b; } 77 | fn subtract(a: i64, b: i64) i64 { return a - b; } 78 | fn multiply(a: i64, b: i64) i64 { return a * b; } 79 | 80 | // function that can return errors. 81 | fn divide(a: i64, b: i64) error{DivideByZero, HeyCantDivide99}!i64 { 82 | if (b == 0) 83 | return error.DivideByZero; // catch a panic and turn it into an error. 84 | if (a == 99) 85 | return error.HeyCantDivide99; 86 | return @divTrunc(a, b); 87 | } 88 | }; 89 | 90 | 91 | fn raiseToPower(a: f64, b: i64) f64 { 92 | return std.math.pow(f64, a, @floatFromInt(b)); 93 | } 94 | 95 | fn logNum(a: i64) void { 96 | std.debug.print("logNum: {}\n", .{a}); 97 | } 98 | 99 | 100 | var g_sum: i64 = 10; // sum starts at 10. The sum variable is passed in as context to functions. 101 | 102 | fn increase(ctx_sum: *i64, a: i64) i64 { 103 | ctx_sum.* += a; 104 | return ctx_sum.*; 105 | } 106 | 107 | fn decrease(ctx_sum: *i64, a: i64) i64 { 108 | ctx_sum.* -= a; 109 | return ctx_sum.*; 110 | } 111 | 112 | 113 | const Stash = struct { 114 | alloc: Allocator, 115 | map: StringHashMap(f64), 116 | 117 | fn init(alloc: Allocator) @This() { 118 | return .{ 119 | .alloc = alloc, 120 | .map = StringHashMap(f64).init(alloc), 121 | }; 122 | } 123 | 124 | fn deinit(self: *@This()) void { 125 | self.map.deinit(); 126 | } 127 | 128 | fn load(self: *@This(), key: []const u8) ?f64 { 129 | return self.map.get(key); 130 | } 131 | 132 | fn save(self: *@This(), key: []const u8, amount: f64) !bool { 133 | const existed = self.map.contains(key); 134 | try self.map.put(key, amount); 135 | return existed; 136 | } 137 | }; 138 | 139 | 140 | const CatInfo = struct { 141 | cat_name: []const u8, 142 | weight: f64, 143 | eye_color: []const u8, 144 | }; 145 | 146 | fn weighCat(cat: CatInfo) []const u8 { 147 | if (std.mem.eql(u8, cat.cat_name, "Garfield")) return "Fat Cat!"; 148 | if (std.mem.eql(u8, cat.cat_name, "Odin")) return "Not a cat!"; 149 | if (0 < cat.weight and cat.weight <= 2.0) return "Tiny cat"; 150 | if (2.0 < cat.weight and cat.weight <= 10.0) return "Normal weight"; 151 | if (10.0 < cat.weight ) return "Heavy cat"; 152 | return "Something wrong"; 153 | } 154 | 155 | fn makeCat(name: []const u8, eye_color: []const u8) CatInfo { 156 | const seed: u64 = @truncate(name.len); 157 | var prng = std.Random.DefaultPrng.init(seed); 158 | return .{ 159 | .cat_name = name, 160 | .weight = @floatFromInt(prng.random().uintAtMost(u32, 20)), 161 | .eye_color = eye_color, 162 | }; 163 | } 164 | 165 | fn cloneCat(dc: *zigjr.DispatchCtx, cat: CatInfo) ![2]CatInfo { 166 | return .{ 167 | cat, 168 | CatInfo { 169 | .cat_name = try std.fmt.allocPrint(dc.arena(), "Clone of {s}", .{cat.cat_name}), 170 | .weight = cat.weight * 2, 171 | .eye_color = cat.eye_color, 172 | }, 173 | }; 174 | } 175 | 176 | fn descCat(cat: CatInfo) struct { []const u8, f64, f64, []const u8 } { 177 | return .{ 178 | cat.cat_name, 179 | cat.weight, 180 | cat.weight * 2, 181 | cat.eye_color, 182 | }; 183 | } 184 | 185 | fn addWeight(weight: f64, cat: CatInfo) CatInfo { 186 | var cat2 = cat; 187 | cat2.weight += weight; 188 | return cat2; 189 | } 190 | 191 | 192 | fn usage() void { 193 | std.debug.print( 194 | \\Usage: calc 195 | \\Usage: calc < message.json 196 | \\ 197 | \\The program reads from stdin. 198 | , .{}); 199 | } 200 | 201 | 202 | 203 | -------------------------------------------------------------------------------- /src/streaming/DupWriter.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const DupWriter = @This(); 10 | 11 | const std = @import("std"); 12 | const Allocator = std.mem.Allocator; 13 | 14 | /// Provides a writer implementing the std.Io.Writer interface that 15 | /// duplicates the data written to two destination writers. 16 | 17 | // The first destination writer to write data. 18 | writer1: *std.Io.Writer, 19 | // The second destination writer to write data. 20 | writer2: *std.Io.Writer, 21 | // Turn on/off writing for the second destination writer. 22 | writer2_enabled: bool = true, 23 | // The std.Io.Writer interface for this writer implementation. 24 | writer_interface: std.Io.Writer, 25 | 26 | 27 | pub fn init(buffer: []u8, writer1: *std.Io.Writer, writer2: *std.Io.Writer) DupWriter { 28 | return .{ 29 | .writer1 = writer1, 30 | .writer2 = writer2, 31 | .writer_interface = .{ 32 | .vtable = &.{ 33 | .drain = drainDup, 34 | .sendFile = sendFileDup, 35 | }, 36 | .buffer = buffer, 37 | .end = 0, 38 | }, 39 | }; 40 | } 41 | 42 | 43 | /// Get the std.Io.Writer interface pointer, to avoid an accidental copy. 44 | pub fn interface(self: *DupWriter) *std.Io.Writer { 45 | return &self.writer_interface; 46 | } 47 | 48 | pub fn enableWriter2(self: *DupWriter, flag: bool) void { 49 | self.writer2_enabled = flag; 50 | } 51 | 52 | fn drainDup(io_w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize { 53 | const dw: *DupWriter = @alignCast(@fieldParentPtr("writer_interface", io_w)); 54 | const buffered = io_w.buffered(); 55 | const total_len = try dw.writer1.writeSplatHeader(buffered, data, splat); 56 | if (dw.writer2_enabled) { 57 | _ = try dw.writer2.writeSplatHeader(buffered, data, splat); 58 | } 59 | _ = io_w.consume(buffered.len); 60 | return total_len; 61 | } 62 | 63 | 64 | fn sendFileDup(io_w: *std.Io.Writer, file_reader: *std.fs.File.Reader, limit: std.Io.Limit) std.Io.Writer.FileError!usize { 65 | const dw: *DupWriter = @alignCast(@fieldParentPtr("writer_interface", io_w)); 66 | const len = try dw.writer1.sendFile(file_reader, limit); 67 | if (dw.writer2_enabled) { 68 | _ = try dw.writer2.sendFile(file_reader, limit); 69 | } 70 | return len; 71 | } 72 | 73 | 74 | test "Write dup with .write()" { 75 | var gpa = std.heap.DebugAllocator(.{}){}; 76 | const alloc = gpa.allocator(); 77 | { 78 | var writer1 = std.Io.Writer.Allocating.init(alloc); 79 | var writer2 = std.Io.Writer.Allocating.init(alloc); 80 | defer writer1.deinit(); 81 | defer writer2.deinit(); 82 | 83 | var buf: [10]u8 = undefined; 84 | var dup_writer = DupWriter.init(&buf, &writer1.writer, &writer2.writer); 85 | const writer = dup_writer.interface(); 86 | _ = try writer.write("abc"); 87 | // std.debug.print("buf: {any}, end: {}\n", .{writer.buffer, writer.end}); 88 | try writer.flush(); 89 | // std.debug.print("w1: |{s}|\n", .{writer1.written()}); 90 | // std.debug.print("w2: |{s}|\n", .{writer2.written()}); 91 | try std.testing.expect(writer1.written().len == 3); 92 | try std.testing.expect(writer2.written().len == 3); 93 | try std.testing.expectEqualSlices(u8, writer1.written(), writer2.written()); 94 | } 95 | if (gpa.detectLeaks()) std.debug.print("Memory leak detected!\n", .{}); 96 | } 97 | 98 | test "Write dup with streaming" { 99 | var gpa = std.heap.DebugAllocator(.{}){}; 100 | const alloc = gpa.allocator(); 101 | { 102 | var reader: std.Io.Reader = .fixed("abc"); 103 | var writer1 = std.Io.Writer.Allocating.init(alloc); 104 | var writer2 = std.Io.Writer.Allocating.init(alloc); 105 | defer writer1.deinit(); 106 | defer writer2.deinit(); 107 | 108 | var buf: [10]u8 = undefined; 109 | var dup_writer = DupWriter.init(&buf, &writer1.writer, &writer2.writer); 110 | const writer = dup_writer.interface(); 111 | _ = try reader.stream(writer, .unlimited); 112 | try writer.flush(); 113 | // std.debug.print("w1: |{s}|\n", .{writer1.written()}); 114 | // std.debug.print("w2: |{s}|\n", .{writer2.written()}); 115 | try std.testing.expect(writer1.written().len == 3); 116 | try std.testing.expect(writer2.written().len == 3); 117 | try std.testing.expectEqualSlices(u8, writer1.written(), writer2.written()); 118 | } 119 | if (gpa.detectLeaks()) std.debug.print("Memory leak detected!\n", .{}); 120 | } 121 | 122 | test "Write dup with streaming, small buffer" { 123 | var gpa = std.heap.DebugAllocator(.{}){}; 124 | const alloc = gpa.allocator(); 125 | { 126 | var reader: std.Io.Reader = .fixed("0123456789012345678901234567890123456789"); 127 | var writer1 = std.Io.Writer.Allocating.init(alloc); 128 | var writer2 = std.Io.Writer.Allocating.init(alloc); 129 | defer writer1.deinit(); 130 | defer writer2.deinit(); 131 | 132 | var buf: [2]u8 = undefined; // small buffer to force flushing intermediate data 133 | var dup_writer = DupWriter.init(&buf, &writer1.writer, &writer2.writer); 134 | const writer = dup_writer.interface(); 135 | _ = try reader.stream(writer, .unlimited); 136 | try writer.flush(); 137 | // std.debug.print("w1: |{s}|\n", .{writer1.written()}); 138 | // std.debug.print("w2: |{s}|\n", .{writer2.written()}); 139 | try std.testing.expect(writer1.written().len == 40); 140 | try std.testing.expect(writer2.written().len == 40); 141 | try std.testing.expectEqualSlices(u8, writer1.written(), writer2.written()); 142 | } 143 | if (gpa.detectLeaks()) std.debug.print("Memory leak detected!\n", .{}); 144 | } 145 | 146 | test "Write dup with .write() and enableWriter2" { 147 | var gpa = std.heap.DebugAllocator(.{}){}; 148 | const alloc = gpa.allocator(); 149 | { 150 | var writer1 = std.Io.Writer.Allocating.init(alloc); 151 | var writer2 = std.Io.Writer.Allocating.init(alloc); 152 | defer writer1.deinit(); 153 | defer writer2.deinit(); 154 | 155 | var buf: [10]u8 = undefined; 156 | var dup_writer = DupWriter.init(&buf, &writer1.writer, &writer2.writer); 157 | const writer = dup_writer.interface(); 158 | _ = try writer.write("a"); 159 | try writer.flush(); 160 | dup_writer.enableWriter2(false); 161 | _ = try writer.write("b"); 162 | try writer.flush(); 163 | dup_writer.enableWriter2(true); 164 | _ = try writer.write("c"); 165 | try writer.flush(); 166 | // std.debug.print("w1: |{s}|\n", .{writer1.written()}); 167 | // std.debug.print("w2: |{s}|\n", .{writer2.written()}); 168 | try std.testing.expect(writer1.written().len == 3); 169 | try std.testing.expect(writer2.written().len == 2); 170 | try std.testing.expectEqualSlices(u8, writer1.written(), "abc"); 171 | try std.testing.expectEqualSlices(u8, writer2.written(), "ac"); 172 | } 173 | if (gpa.detectLeaks()) std.debug.print("Memory leak detected!\n", .{}); 174 | } 175 | 176 | -------------------------------------------------------------------------------- /src/rpc/dispatcher.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const std = @import("std"); 10 | const Allocator = std.mem.Allocator; 11 | const allocPrint = std.fmt.allocPrint; 12 | const ArrayList = std.ArrayList; 13 | 14 | const zigjr = @import("../zigjr.zig"); 15 | const RpcRequest = zigjr.RpcRequest; 16 | const RpcResponse = zigjr.RpcResponse; 17 | const ErrorCode = zigjr.errors.ErrorCode; 18 | 19 | 20 | pub const nop_request = RpcRequest{}; 21 | 22 | 23 | pub const DispatchCtxImpl = struct { 24 | arena: Allocator, // per-request arena allocator 25 | logger: zigjr.Logger, // logger is usually set to the passed in logger in RequestPipeline or in the stream API. 26 | // per-request request and result 27 | request: *const zigjr.RpcRequest = &nop_request, 28 | result: zigjr.DispatchResult = .asNone(), 29 | err: ?anyerror = null, 30 | // per-request user data; set up by the pre-request and cleaned up by the end-request hook. 31 | user_props: *anyopaque = undefined, 32 | 33 | // TODO: add session id, session manager, and others. 34 | // session_id: SessionId, 35 | // session_mgr: *SessionMgr, 36 | // session_arena: Allocator, 37 | 38 | pub fn reset(self: *DispatchCtxImpl) void { 39 | self.request = &nop_request; 40 | self.result = .asNone(); 41 | self.err = null; 42 | // TODO: clean up self.user_props? 43 | } 44 | }; 45 | 46 | 47 | /// RequestDispatcher interface 48 | /// This is for the request handlers in a RPC server handling the incoming requests. 49 | pub const RequestDispatcher = struct { 50 | impl_ptr: *anyopaque, 51 | dispatch_fn: *const fn(impl_ptr: *anyopaque, dc: *DispatchCtxImpl) anyerror!DispatchResult, 52 | dispatchEnd_fn: *const fn(impl_ptr: *anyopaque, dc: *DispatchCtxImpl) void, 53 | 54 | // Interface is implemented by the 'impl' object. 55 | pub fn implBy(impl_obj: anytype) RequestDispatcher { 56 | const ImplType = @TypeOf(impl_obj); 57 | if (@typeInfo(ImplType) != .pointer) 58 | @compileError("impl_obj should be a pointer, but its type is " ++ @typeName(ImplType)); 59 | 60 | const Delegate = struct { 61 | fn dispatch(impl_ptr: *anyopaque, dc: *DispatchCtxImpl) anyerror!DispatchResult { 62 | const impl: ImplType = @ptrCast(@alignCast(impl_ptr)); 63 | return impl.dispatch(dc); 64 | } 65 | 66 | fn dispatchEnd(impl_ptr: *anyopaque, dc: *DispatchCtxImpl) void { 67 | const impl: ImplType = @ptrCast(@alignCast(impl_ptr)); 68 | return impl.dispatchEnd(dc); 69 | } 70 | }; 71 | 72 | return .{ 73 | .impl_ptr = impl_obj, 74 | .dispatch_fn = Delegate.dispatch, 75 | .dispatchEnd_fn = Delegate.dispatchEnd, 76 | }; 77 | } 78 | 79 | // The implementation must have these methods. 80 | 81 | pub fn dispatch(self: RequestDispatcher, dc: *DispatchCtxImpl) anyerror!DispatchResult { 82 | return self.dispatch_fn(self.impl_ptr, dc); 83 | } 84 | 85 | pub fn dispatchEnd(self: RequestDispatcher, dc: *DispatchCtxImpl) void { 86 | return self.dispatchEnd_fn(self.impl_ptr, dc); 87 | } 88 | }; 89 | 90 | 91 | /// ResponseDispatcher interface 92 | /// This is for the response handlers in a RPC client handling the returned responses. 93 | pub const ResponseDispatcher = struct { 94 | impl_ptr: *anyopaque, 95 | dispatch_fn: *const fn(impl_ptr: *anyopaque, alloc: Allocator, res: RpcResponse) anyerror!bool, 96 | 97 | // Interface is implemented by the 'impl' object. 98 | pub fn implBy(impl_obj: anytype) ResponseDispatcher { 99 | const ImplType = @TypeOf(impl_obj); 100 | 101 | const Delegate = struct { 102 | fn dispatch(impl_ptr: *anyopaque, alloc: Allocator, res: RpcResponse) anyerror!bool { 103 | const impl: ImplType = @ptrCast(@alignCast(impl_ptr)); 104 | return impl.dispatch(alloc, res); 105 | } 106 | }; 107 | 108 | return .{ 109 | .impl_ptr = impl_obj, 110 | .dispatch_fn = Delegate.dispatch, 111 | }; 112 | } 113 | 114 | pub fn dispatch(self: @This(), alloc: Allocator, res: RpcResponse) anyerror!bool { 115 | return self.dispatch_fn(self.impl_ptr, alloc, res); 116 | } 117 | }; 118 | 119 | 120 | /// The returning result from dispatcher.dispatch(). 121 | /// For the result JSON string and the err.data JSON string, it's best that they're produced by 122 | /// std.json.stringifyAlloc() to ensure a valid JSON string. 123 | /// The DispatchResult object is cleaned up at the dispatchEnd() stage. 124 | pub const DispatchResult = union(enum) { 125 | const Self = @This(); 126 | 127 | none: void, // No result, for notification call. 128 | end_stream: void, // Handler wants to end the current streaming session. 129 | result: []const u8, // JSON string for result value. 130 | err: struct { 131 | code: ErrorCode, 132 | msg: []const u8 = "", // Error text string. 133 | data: ?[]const u8 = null, // JSON string for additional error data value. 134 | }, 135 | 136 | /// Create a DispatchResult with no result, for JSON-RPC notification. 137 | pub fn asNone() Self { 138 | return .{ .none = {} }; 139 | } 140 | 141 | /// Create a DispatchResult with end_stream to end the current streaming session. 142 | pub fn asEndStream() Self { 143 | return .{ .end_stream = {} }; 144 | } 145 | 146 | /// Create a DispatchResult with a result encoded in a JSON string. 147 | pub fn withResult(json: []const u8) Self { 148 | return .{ .result = json }; 149 | } 150 | 151 | /// Create a DispatchResult with an error. 152 | pub fn withErr(code: ErrorCode, msg: []const u8) Self { 153 | return .{ 154 | .err = .{ 155 | .code = code, 156 | .msg = msg, 157 | } 158 | }; 159 | } 160 | 161 | /// Create a DispatchResult with the parse error from RpcRequest. 162 | /// DispatchResult references memory in RpcRequest. Their lifetime must be managed together. 163 | pub fn withRequestErr(req: *const RpcRequest) Self { 164 | return .{ 165 | .err = .{ 166 | .code = req.err().code, 167 | .msg = req.err().err_msg, 168 | }, 169 | }; 170 | } 171 | 172 | /// Create a DispatchResult with the error of anyerror type. 173 | pub fn withAnyErr(err: anyerror) Self { 174 | return switch (err) { 175 | DispatchErrors.MethodNotFound => Self.withErr( 176 | ErrorCode.MethodNotFound, "Method not found."), 177 | DispatchErrors.InvalidParams => Self.withErr( 178 | ErrorCode.InvalidParams, "Invalid parameters."), 179 | DispatchErrors.NoHandlerForObjectParam => Self.withErr( 180 | ErrorCode.InvalidParams, "Handler expecting an object parameter but got non-object parameters."), 181 | DispatchErrors.MismatchedParamCounts => Self.withErr( 182 | ErrorCode.InvalidParams, "The number of parameters of the request does not match the parameter count of the handler."), 183 | else => Self.withErr(ErrorCode.ServerError, @errorName(err)), 184 | }; 185 | } 186 | 187 | }; 188 | 189 | // Force a pointer type to a const pointer type. 190 | fn ptrToConstPtrType(comptime T: type) type { 191 | var ptr = @typeInfo(T).pointer; 192 | ptr.is_const = true; 193 | return @Type(.{ 194 | .pointer = ptr, 195 | }); 196 | } 197 | 198 | 199 | pub const DispatchErrors = error { 200 | NoHandlerForArrayParam, 201 | NoHandlerForObjectParam, 202 | MismatchedParamCounts, 203 | MethodNotFound, 204 | InvalidParams, 205 | WrongRequestParamTypeForRawParams, 206 | OutOfMemory, 207 | }; 208 | 209 | 210 | -------------------------------------------------------------------------------- /examples/stream_calc.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const std = @import("std"); 10 | const Allocator = std.mem.Allocator; 11 | const ParseOptions = std.json.ParseOptions; 12 | const StringHashMap = std.hash_map.StringHashMap; 13 | 14 | const zigjr = @import("zigjr"); 15 | 16 | 17 | pub fn main() !void { 18 | var gpa = std.heap.DebugAllocator(.{}){}; 19 | defer _ = gpa.deinit(); 20 | const alloc = gpa.allocator(); 21 | var args = try CmdArgs.init(alloc); 22 | defer args.deinit(); 23 | args.parse() catch { usage(); return; }; 24 | try runExample(alloc, args); 25 | } 26 | 27 | fn runExample(alloc: Allocator, args: CmdArgs) !void { 28 | var stash = Stash.init(alloc); 29 | defer stash.deinit(); 30 | 31 | var rpc_dispatcher = try zigjr.RpcDispatcher.init(alloc); 32 | defer rpc_dispatcher.deinit(); 33 | const dispatcher = zigjr.RequestDispatcher.implBy(&rpc_dispatcher); 34 | 35 | try rpc_dispatcher.add("add", Basic.add); // register functions in a struct scope. 36 | try rpc_dispatcher.add("subtract", Basic.subtract); 37 | try rpc_dispatcher.add("multiply", Basic.multiply); 38 | try rpc_dispatcher.add("divide", Basic.divide); 39 | try rpc_dispatcher.add("pow", raiseToPower); // register function from any scope. 40 | try rpc_dispatcher.add("logNum", logNum); // function with no result. 41 | try rpc_dispatcher.addWithCtx("inc", &g_sum, increase); // attach a context to the function. 42 | try rpc_dispatcher.addWithCtx("dec", &g_sum, decrease); // attach the same context to another function. 43 | try rpc_dispatcher.addWithCtx("load", &stash, Stash.load); // handler on a struct object context. 44 | try rpc_dispatcher.addWithCtx("save", &stash, Stash.save); // handler on a struct object context. 45 | try rpc_dispatcher.add("weigh-cat", weighCat); // function with a struct parameter. 46 | try rpc_dispatcher.add("make-cat", makeCat); // function returns a struct parameter. 47 | try rpc_dispatcher.add("clone-cat", cloneCat); // function returns an array. 48 | try rpc_dispatcher.add("desc-cat", descCat); // function returns a tuple. 49 | try rpc_dispatcher.add("add-weight", addWeight); 50 | 51 | var stdin_buffer: [1024]u8 = undefined; 52 | var stdin_reader = std.fs.File.stdin().reader(&stdin_buffer); 53 | const stdin = &stdin_reader.interface; 54 | 55 | var stdout_buffer: [1024]u8 = undefined; 56 | var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); 57 | const stdout = &stdout_writer.interface; 58 | 59 | var my_logger = MyLogger{}; 60 | if (args.by_delimiter) { 61 | // Handle streaming of requests separated by a delimiter (LF). 62 | try zigjr.stream.requestsByDelimiter(alloc, stdin, stdout, dispatcher, 63 | .{ .logger = zigjr.Logger.implBy(&my_logger) }); 64 | } else if (args.by_length) { 65 | // Handle streaming of requests separated by the Content-Length header. 66 | try zigjr.stream.requestsByContentLength(alloc, stdin, stdout, dispatcher, .{}); 67 | } else { 68 | usage(); 69 | } 70 | try stdout.flush(); 71 | } 72 | 73 | const Basic = struct { 74 | fn add(a: i64, b: i64) i64 { return a + b; } 75 | fn subtract(a: i64, b: i64) i64 { return a - b; } 76 | fn multiply(a: i64, b: i64) i64 { return a * b; } 77 | 78 | // function that can return errors. 79 | fn divide(a: i64, b: i64) error{DivideByZero, HeyCantDivide99}!i64 { 80 | if (b == 0) 81 | return error.DivideByZero; // catch a panic and turn it into an error. 82 | if (a == 99) 83 | return error.HeyCantDivide99; 84 | return @divTrunc(a, b); 85 | } 86 | }; 87 | 88 | 89 | fn raiseToPower(a: f64, b: i64) f64 { 90 | return std.math.pow(f64, a, @floatFromInt(b)); 91 | } 92 | 93 | fn logNum(a: i64) void { 94 | std.debug.print("logNum: {}\n", .{a}); 95 | } 96 | 97 | 98 | var g_sum: i64 = 10; // sum starts at 10. The sum variable is passed in as context to functions. 99 | 100 | fn increase(ctx_sum: *i64, a: i64) i64 { 101 | ctx_sum.* += a; 102 | return ctx_sum.*; 103 | } 104 | 105 | fn decrease(ctx_sum: *i64, a: i64) i64 { 106 | ctx_sum.* -= a; 107 | return ctx_sum.*; 108 | } 109 | 110 | 111 | const Stash = struct { 112 | alloc: Allocator, 113 | map: StringHashMap(f64), 114 | 115 | fn init(alloc: Allocator) @This() { 116 | return .{ 117 | .alloc = alloc, 118 | .map = StringHashMap(f64).init(alloc), 119 | }; 120 | } 121 | 122 | fn deinit(self: *@This()) void { 123 | self.map.deinit(); 124 | } 125 | 126 | fn load(self: *@This(), key: []const u8) ?f64 { 127 | return self.map.get(key); 128 | } 129 | 130 | fn save(self: *@This(), key: []const u8, amount: f64) !bool { 131 | const existed = self.map.contains(key); 132 | try self.map.put(key, amount); 133 | return existed; 134 | } 135 | }; 136 | 137 | 138 | const CatInfo = struct { 139 | cat_name: []const u8, 140 | weight: f64, 141 | eye_color: []const u8, 142 | }; 143 | 144 | fn weighCat(cat: CatInfo) []const u8 { 145 | if (std.mem.eql(u8, cat.cat_name, "Garfield")) return "Fat Cat!"; 146 | if (std.mem.eql(u8, cat.cat_name, "Odin")) return "Not a cat!"; 147 | if (0 < cat.weight and cat.weight <= 2.0) return "Tiny cat"; 148 | if (2.0 < cat.weight and cat.weight <= 10.0) return "Normal weight"; 149 | if (10.0 < cat.weight ) return "Heavy cat"; 150 | return "Something wrong"; 151 | } 152 | 153 | fn makeCat(name: []const u8, eye_color: []const u8) CatInfo { 154 | const seed: u64 = @truncate(name.len); 155 | var prng = std.Random.DefaultPrng.init(seed); 156 | return .{ 157 | .cat_name = name, 158 | .weight = @floatFromInt(prng.random().uintAtMost(u32, 20)), 159 | .eye_color = eye_color, 160 | }; 161 | } 162 | 163 | fn cloneCat(dc: *zigjr.DispatchCtx, cat: CatInfo) ![2]CatInfo { 164 | return .{ 165 | cat, 166 | CatInfo { 167 | .cat_name = try std.fmt.allocPrint(dc.arena(), "Clone of {s}", .{cat.cat_name}), 168 | .weight = cat.weight * 2, 169 | .eye_color = cat.eye_color, 170 | }, 171 | }; 172 | } 173 | 174 | fn descCat(cat: CatInfo) struct { []const u8, f64, f64, []const u8 } { 175 | return .{ 176 | cat.cat_name, 177 | cat.weight, 178 | cat.weight * 2, 179 | cat.eye_color, 180 | }; 181 | } 182 | 183 | fn addWeight(weight: f64, cat: CatInfo) CatInfo { 184 | var cat2 = cat; 185 | cat2.weight += weight; 186 | return cat2; 187 | } 188 | 189 | 190 | const MyLogger = struct { 191 | count: usize = 0, 192 | 193 | pub fn start(_: @This(), _: []const u8) void {} 194 | pub fn log(self: *@This(), source:[] const u8, operation: []const u8, message: []const u8) void { 195 | self.count += 1; 196 | std.debug.print("LOG {}: {s} - {s} - {s}\n", .{self.count, source, operation, message}); 197 | } 198 | pub fn stop(_: @This(), _: []const u8) void {} 199 | 200 | }; 201 | 202 | 203 | fn usage() void { 204 | std.debug.print( 205 | \\Usage: calc_stream --by-delimiter < messages_by_lf.json 206 | \\Usage: calc_stream --by-length < messages_by_length.json 207 | \\ 208 | \\The program reads from stdin. 209 | , .{}); 210 | } 211 | 212 | // Poorman's quick and dirty command line argument parsing. 213 | const CmdArgs = struct { 214 | const Self = @This(); 215 | 216 | arg_itr: std.process.ArgIterator, 217 | by_delimiter: bool = false, 218 | by_length: bool = false, 219 | 220 | fn init(allocator: Allocator) !CmdArgs { 221 | var args = CmdArgs { 222 | .arg_itr = try std.process.argsWithAllocator(allocator), 223 | }; 224 | _ = args.arg_itr.next(); 225 | return args; 226 | } 227 | 228 | fn deinit(self: *CmdArgs) void { 229 | self.arg_itr.deinit(); 230 | } 231 | 232 | fn parse(self: *Self) !void { 233 | var argv = self.arg_itr; 234 | while (argv.next())|argz| { 235 | const arg = std.mem.sliceTo(argz, 0); 236 | if (std.mem.eql(u8, arg, "--by-delimiter")) { 237 | self.by_delimiter = true; 238 | } else if (std.mem.eql(u8, arg, "--by-length")) { 239 | self.by_length = true; 240 | } 241 | } 242 | } 243 | 244 | }; 245 | 246 | 247 | -------------------------------------------------------------------------------- /src/tests/message_tests.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const testing = std.testing; 3 | const allocPrint = std.fmt.allocPrint; 4 | const Allocator = std.mem.Allocator; 5 | const ArrayList = std.ArrayList; 6 | const nanoTimestamp = std.time.nanoTimestamp; 7 | const Value = std.json.Value; 8 | 9 | const zigjr = @import("../zigjr.zig"); 10 | const RpcRequestMessage = zigjr.RpcRequestMessage; 11 | const RpcRequest = zigjr.RpcRequest; 12 | const RpcResponse = zigjr.RpcResponse; 13 | const RpcMessageResult = zigjr.RpcRequest; 14 | const RequestDispatcher = zigjr.RequestDispatcher; 15 | const ResponseDispatcher = zigjr.ResponseDispatcher; 16 | const DispatchResult = zigjr.DispatchResult; 17 | const ErrorCode = zigjr.ErrorCode; 18 | const JrErrors = zigjr.JrErrors; 19 | 20 | 21 | test "Parsing valid request, single integer param, integer id" { 22 | var gpa = std.heap.DebugAllocator(.{}){}; 23 | defer _ = gpa.deinit(); 24 | const alloc = gpa.allocator(); 25 | 26 | { 27 | var msg_result = zigjr.parseRpcMessage(alloc, 28 | \\{"jsonrpc": "2.0", "method": "fun0", "params": [42], "id": 1} 29 | ); 30 | defer msg_result.deinit(); 31 | var result = msg_result.request_result; 32 | const req = try result.request(); 33 | try testing.expect(@TypeOf(result.request_msg) == RpcRequestMessage); 34 | try testing.expect(result.request_msg == .request); 35 | switch (result.request_msg) { 36 | .request => |r| { _=r; try testing.expect(true); }, 37 | .batch => |b| { _=b; try testing.expect(false); }, 38 | } 39 | try testing.expect(result.isRequest()); 40 | try testing.expect(!result.isBatch()); 41 | try testing.expect(result.batch() == JrErrors.NotBatchRpcRequest); 42 | try testing.expect(std.mem.eql(u8, &req.jsonrpc, "2.0")); 43 | try testing.expect(std.mem.eql(u8, req.method, "fun0")); 44 | try testing.expect(req.hasParams()); 45 | try testing.expect(req.params == .array); 46 | try testing.expect(req.params.array.items.len == 1); 47 | try testing.expect(req.params.array.items[0].integer == 42); 48 | try testing.expect(req.hasArrayParams()); 49 | try testing.expect(!req.hasObjectParams()); 50 | try testing.expect(req.arrayParams() != null); 51 | try testing.expect(req.objectParams() == null); 52 | try testing.expect(req.arrayParams().?.items.len == 1); 53 | try testing.expect(req.arrayParams().?.items[0].integer == 42); 54 | try testing.expect(req.id.isValid()); 55 | try testing.expect(req.id.eql(1)); 56 | try testing.expect(req.hasError() == false); 57 | } 58 | 59 | } 60 | 61 | test "Parsing valid request, single string param, string id" { 62 | var gpa = std.heap.DebugAllocator(.{}){}; 63 | defer _ = gpa.deinit(); 64 | const alloc = gpa.allocator(); 65 | 66 | { 67 | var msg_result = zigjr.parseRpcMessage(alloc, 68 | \\{"jsonrpc": "2.0", "method": "fun1", "params": ["FUN1"], "id": "1"} 69 | ); 70 | defer msg_result.deinit(); 71 | var result = msg_result.request_result; 72 | const req = try result.request(); 73 | try testing.expect(@TypeOf(result.request_msg) == RpcRequestMessage); 74 | try testing.expect(result.request_msg == .request); 75 | switch (result.request_msg) { 76 | .request => |r| { _=r; try testing.expect(true); }, 77 | .batch => |b| { _=b; try testing.expect(false); }, 78 | } 79 | try testing.expect(result.isRequest()); 80 | try testing.expect(!result.isBatch()); 81 | try testing.expect(result.batch() == JrErrors.NotBatchRpcRequest); 82 | try testing.expect(std.mem.eql(u8, &req.jsonrpc, "2.0")); 83 | try testing.expect(std.mem.eql(u8, req.method, "fun1")); 84 | try testing.expect(req.hasParams()); 85 | try testing.expect(req.params == .array); 86 | try testing.expect(req.params.array.items.len == 1); 87 | try testing.expect(std.mem.eql(u8, req.params.array.items[0].string, "FUN1")); 88 | try testing.expect(req.hasArrayParams()); 89 | try testing.expect(!req.hasObjectParams()); 90 | try testing.expect(req.arrayParams() != null); 91 | try testing.expect(req.objectParams() == null); 92 | try testing.expect(req.arrayParams().?.items.len == 1); 93 | try testing.expect(std.mem.eql(u8, req.arrayParams().?.items[0].string, "FUN1")); 94 | try testing.expect(req.id.isValid()); 95 | try testing.expect(req.id.eql("1")); 96 | try testing.expect(req.id.eql([_]u8{'1'})); 97 | try testing.expect(req.hasError() == false); 98 | } 99 | 100 | } 101 | 102 | 103 | 104 | const HelloDispatcher = struct { 105 | pub fn dispatch(_: *@This(), dc: *zigjr.DispatchCtxImpl) !DispatchResult { 106 | if (std.mem.eql(u8, dc.request.method, "hello")) { 107 | return .{ 108 | .result = "\"hello back\"", 109 | }; 110 | } else { 111 | return .{ 112 | .err = .{ 113 | .code = ErrorCode.MethodNotFound, 114 | .msg = "Method not found.", 115 | } 116 | }; 117 | } 118 | } 119 | 120 | pub fn dispatchEnd(_: *@This(), _: *zigjr.DispatchCtxImpl) void { 121 | } 122 | }; 123 | 124 | 125 | test "Parse response to a request of hello method via " { 126 | var gpa = std.heap.DebugAllocator(.{}){}; 127 | defer _ = gpa.deinit(); 128 | const alloc = gpa.allocator(); 129 | 130 | { 131 | var impl = HelloDispatcher{}; 132 | var pipeline = try zigjr.RequestPipeline.init(alloc, RequestDispatcher.implBy(&impl), null); 133 | defer pipeline.deinit(); 134 | 135 | _ = try pipeline.runRequest( 136 | \\{"jsonrpc": "2.0", "method": "hello", "params": [42], "id": 1} 137 | ); 138 | var msg_result = zigjr.parseRpcMessage(alloc, pipeline.responseJson()); 139 | defer msg_result.deinit(); 140 | var parsed_res = msg_result.response_result; 141 | const res = try parsed_res.response(); 142 | // std.debug.print("res.result: {s}\n", .{res.result.string}); 143 | 144 | try testing.expectEqualSlices(u8, res.result.string, "hello back"); 145 | try testing.expect(res.resultEql("hello back")); 146 | try testing.expect(res.id.eql(1)); 147 | } 148 | 149 | } 150 | 151 | test "Parse error from a request of unknown method, expect error" { 152 | var gpa = std.heap.DebugAllocator(.{}){}; 153 | defer _ = gpa.deinit(); 154 | const alloc = gpa.allocator(); 155 | 156 | { 157 | var impl = HelloDispatcher{}; 158 | var pipeline = try zigjr.RequestPipeline.init(alloc, RequestDispatcher.implBy(&impl), null); 159 | defer pipeline.deinit(); 160 | 161 | _ = try pipeline.runRequest( 162 | \\{"jsonrpc": "2.0", "method": "non-hello", "params": [42], "id": 1} 163 | ); 164 | 165 | var msg_result = zigjr.parseRpcMessage(alloc, pipeline.responseJson()); 166 | defer msg_result.deinit(); 167 | var parsed_res = msg_result.response_result; 168 | const res = try parsed_res.response(); 169 | 170 | try testing.expect(res.hasErr()); 171 | try testing.expectEqual(res.err().code, @intFromEnum(ErrorCode.MethodNotFound)); 172 | try testing.expect(res.id.eql(1)); 173 | } 174 | 175 | } 176 | 177 | 178 | test "Dispatch on the request and response" { 179 | var gpa = std.heap.DebugAllocator(.{}){}; 180 | defer _ = gpa.deinit(); 181 | const alloc = gpa.allocator(); 182 | 183 | { 184 | var req_dispatcher = HelloDispatcher{}; 185 | 186 | var res_dispatcher = struct { 187 | pub fn dispatch(_: *@This(), _: Allocator, res: RpcResponse) anyerror!bool { 188 | // std.debug.print("response: {any}\n", .{res}); 189 | try testing.expectEqualSlices(u8, res.result.string, "hello back"); 190 | try testing.expect(res.id.eql(1)); 191 | return true; 192 | } 193 | } {}; 194 | 195 | var pipeline = try zigjr.pipeline.MessagePipeline.init(alloc, RequestDispatcher.implBy(&req_dispatcher), 196 | ResponseDispatcher.implBy(&res_dispatcher), 197 | null); 198 | defer pipeline.deinit(); 199 | 200 | const req_run_status = try pipeline.runMessage( 201 | \\{"jsonrpc": "2.0", "method": "hello", "params": [42], "id": 1} 202 | ); 203 | try testing.expect(req_run_status.kind == .request); 204 | // std.debug.print("run_result: {}\n", .{run_result}); 205 | // std.debug.print("response_buf: {s}\n", .{pipeline.responseJson()}); 206 | 207 | // Feed the response from request back into the message pipeline. 208 | const res_run_status = try pipeline.runMessage(pipeline.reqResponseJson()); 209 | try testing.expect(res_run_status.kind == .response); 210 | 211 | } 212 | 213 | } 214 | 215 | -------------------------------------------------------------------------------- /src/jsonrpc/response.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const std = @import("std"); 10 | const Allocator = std.mem.Allocator; 11 | const allocPrint = std.fmt.allocPrint; 12 | const Scanner = std.json.Scanner; 13 | const ParseOptions = std.json.ParseOptions; 14 | const innerParse = std.json.innerParse; 15 | const ParseError = std.json.ParseError; 16 | const Value = std.json.Value; 17 | 18 | const req_parser = @import("request.zig"); 19 | const RpcId = req_parser.RpcId; 20 | const errors = @import("errors.zig"); 21 | const ErrorCode = errors.ErrorCode; 22 | const JrErrors = errors.JrErrors; 23 | const Owned = @import("../rpc/deiniter.zig").Owned; 24 | 25 | 26 | /// Parse response_json into a RpcResponseResult. 27 | /// Caller manages the lifetime response_json. Needs to ensure response_json is not 28 | /// freed before RpcResponseResult.deinit(). Parsed result references response_json. 29 | pub fn parseRpcResponse(alloc: Allocator, response_json: []const u8) RpcResponseResult { 30 | return parseRpcResponseOpts(alloc, response_json, .{ 31 | .ignore_unknown_fields = true, 32 | }); 33 | } 34 | 35 | fn parseRpcResponseOpts(alloc: Allocator, response_json: []const u8, opts: ParseOptions) RpcResponseResult { 36 | const json = std.mem.trim(u8, response_json, " "); 37 | if (json.len == 0) { 38 | return .{ .response_msg = .{ .none = {} } }; 39 | } 40 | const parsed = std.json.parseFromSlice(RpcResponseMessage, alloc, json, opts) catch |err| { 41 | // Return an empty response with the error so callers can have a uniform handling. 42 | return .{ .response_msg = .{ .response = RpcResponse.ofParseErr(err) } }; 43 | }; 44 | return .{ 45 | .response_msg = parsed.value, 46 | ._parsed = parsed, 47 | }; 48 | } 49 | 50 | /// Parse response_json into a RpcResponseResult. 51 | /// Caller transfers ownership of response_json to RpcResponseResult. 52 | /// They will be freed in the RpcResponseResult.deinit(). 53 | pub fn parseRpcResponseOwned(alloc: Allocator, response_json: []const u8, opts: ParseOptions) error{OutOfMemory}!RpcResponseResult { 54 | const dup_json = try alloc.dupe(u8, response_json); 55 | var rresult = parseRpcResponseOpts(alloc, dup_json, opts); 56 | rresult.jsonOwned(dup_json, alloc); 57 | return rresult; 58 | } 59 | 60 | pub const RpcResponseResult = struct { 61 | const Self = @This(); 62 | response_msg: RpcResponseMessage = .{ .none = {} }, 63 | _parsed: ?std.json.Parsed(RpcResponseMessage) = null, 64 | _response_json: Owned([]const u8) = .{}, 65 | 66 | pub fn deinit(self: *Self) void { 67 | if (self._parsed) |parsed| parsed.deinit(); 68 | self._response_json.deinit(); 69 | } 70 | 71 | fn jsonOwned(self: *Self, response_json: []const u8, alloc: Allocator) void { 72 | self._response_json = Owned([]const u8).init(response_json, alloc); 73 | } 74 | 75 | pub fn isResponse(self: Self) bool { 76 | return self.response_msg == .response; 77 | } 78 | 79 | pub fn isBatch(self: Self) bool { 80 | return self.response_msg == .batch; 81 | } 82 | 83 | pub fn isNone(self: Self) bool { 84 | return self.response_msg == .none; 85 | } 86 | 87 | /// Shortcut to access the inner tagged union invariant response. 88 | pub fn response(self: *Self) !RpcResponse { 89 | return if (self.isResponse()) self.response_msg.response else JrErrors.NotSingleRpcResponse; 90 | } 91 | 92 | /// Shortcut to access the inner tagged union invariant batch. 93 | pub fn batch(self: *Self) ![]const RpcResponse { 94 | return if (self.isBatch()) self.response_msg.batch else JrErrors.NotBatchRpcResponse; 95 | } 96 | 97 | }; 98 | 99 | pub const RpcResponseMessage = union(enum) { 100 | response: RpcResponse, // JSON-RPC's single response. 101 | batch: []RpcResponse, // JSON-RPC's batch of responses. 102 | none: void, // signifies no response, i.e. notification. 103 | 104 | // Custom parsing when the JSON parser encounters a field of this type. 105 | pub fn jsonParse(alloc: Allocator, source: anytype, options: ParseOptions) !RpcResponseMessage { 106 | switch (try source.peekNextTokenType()) { 107 | .object_begin => { 108 | var res = try innerParse(RpcResponse, alloc, source, options); 109 | res.validate(); 110 | return .{ .response = res }; 111 | }, 112 | .array_begin => { 113 | const batch = try innerParse([]RpcResponse, alloc, source, options); 114 | for (batch)|*res| res.validate(); 115 | return .{ .batch = batch }; 116 | }, 117 | else => return error.UnexpectedToken, 118 | } 119 | } 120 | }; 121 | 122 | pub const RpcResponse = struct { 123 | const Self = @This(); 124 | jsonrpc: [3]u8 = "0.0".*, // default to fail validation. 125 | id: RpcId = .{ .null = {} }, // default for optional field. 126 | result: Value = .{ .null = {} }, // default for optional field. 127 | @"error": RpcResponseError = .{}, // parse error and validation error. 128 | 129 | fn ofParseErr(parse_err: ParseError(Scanner)) Self { 130 | var empty_res = RpcResponse{}; 131 | empty_res.@"error" = RpcResponseError.fromParseError(parse_err); 132 | return empty_res; 133 | } 134 | 135 | fn validate(self: *Self) void { 136 | if (RpcResponseError.validateResponse(self)) |e| { 137 | self.@"error" = e; 138 | } 139 | } 140 | 141 | pub fn err(self: Self) RpcResponseError { 142 | return self.@"error"; 143 | } 144 | 145 | pub fn hasResult(self: Self) bool { 146 | return self.result != .null; 147 | } 148 | 149 | pub fn hasErr(self: Self) bool { 150 | return self.err().code != 0; 151 | } 152 | 153 | pub fn resultEql(self: Self, value: anytype) bool { 154 | return jsonValueEql(self.result, value); 155 | } 156 | 157 | }; 158 | 159 | pub const RpcResponseError = struct { 160 | const Self = @This(); 161 | 162 | code: i32 = 0, 163 | message: []const u8 = "", 164 | data: ?Value = null, 165 | 166 | fn fromParseError(parse_err: ParseError(Scanner)) Self { 167 | return switch (parse_err) { 168 | error.MissingField, error.UnknownField, error.DuplicateField, 169 | error.LengthMismatch, error.UnexpectedEndOfInput => .{ 170 | .code = @intFromEnum(ErrorCode.InvalidRequest), 171 | .message = @errorName(parse_err), 172 | }, 173 | error.Overflow, error.OutOfMemory => .{ 174 | .code = @intFromEnum(ErrorCode.InternalError), 175 | .message = @errorName(parse_err), 176 | }, 177 | else => .{ 178 | .code = @intFromEnum(ErrorCode.ParseError), 179 | .message = @errorName(parse_err), 180 | }, 181 | }; 182 | } 183 | 184 | fn validateResponse(body: *RpcResponse) ?Self { 185 | if (!std.mem.eql(u8, &body.jsonrpc, "2.0")) { 186 | return .{ 187 | .code = @intFromEnum(ErrorCode.InvalidRequest), 188 | .message = "Invalid JSON-RPC version. Must be 2.0.", 189 | }; 190 | } 191 | return null; // return null RpcRequestError for validation passed. 192 | } 193 | }; 194 | 195 | /// Best effort comparison against the JSON Value. 196 | pub fn jsonValueEql(json_value: Value, value: anytype) bool { 197 | const value_info = @typeInfo(@TypeOf(value)); 198 | switch (value_info) { 199 | .null => { 200 | switch (json_value) { 201 | .null => return true, 202 | else => return false, 203 | } 204 | }, 205 | .bool => { 206 | switch (json_value) { 207 | .bool => return json_value.bool == value, 208 | else => return false, 209 | } 210 | }, 211 | .comptime_int, 212 | .int => { 213 | switch (json_value) { 214 | .integer => return json_value.integer == value, 215 | .float => return json_value.float == @as(f64, @floatFromInt(value)), 216 | else => return false, 217 | } 218 | }, 219 | .comptime_float, 220 | .float => { 221 | switch (json_value) { 222 | .integer => return @as(f64, @floatFromInt(json_value.integer)) == value, 223 | .float => return json_value.float == value, 224 | else => return false, 225 | } 226 | }, 227 | .pointer => { 228 | const elem_info = @typeInfo(value_info.pointer.child); 229 | switch (json_value) { 230 | .string => return elem_info == .array and elem_info.array.child == u8 and 231 | std.mem.eql(u8, json_value.string, value), 232 | else => return false, 233 | } 234 | }, 235 | .array => { 236 | switch (json_value) { 237 | .string => return value_info.array.child == u8 and 238 | std.mem.eql(u8, json_value.string, &value), 239 | else => return false, 240 | } 241 | }, 242 | else => { 243 | // std.debug.print("value type info: {any}\n", .{value_info}); 244 | // return false; 245 | @compileError("Only simple value can only be compared."); 246 | }, 247 | } 248 | } 249 | 250 | 251 | -------------------------------------------------------------------------------- /src/jsonrpc/composer.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const std = @import("std"); 10 | const Allocator = std.mem.Allocator; 11 | 12 | const RpcId = @import("request.zig").RpcId; 13 | const errors = @import("errors.zig"); 14 | const ErrorCode = errors.ErrorCode; 15 | const JrErrors = errors.JrErrors; 16 | const WriteAllocError = errors.WriteAllocError; 17 | 18 | 19 | /// Write a request message in JSON string to the writer. 20 | pub fn writeRequestJson(method: []const u8, params_json: ?[]const u8, id: RpcId, 21 | writer: *std.Io.Writer) std.Io.Writer.Error!void { 22 | // TODO: Rewrite. Output jsonrpc, method; switch on id; then params_json. 23 | if (params_json) |params| { 24 | switch (id) { 25 | .num => try writer.print( 26 | \\{{"jsonrpc": "2.0", "method": "{s}", "params": {s}, "id": {}}} 27 | , .{method, params, id.num}), 28 | .str => try writer.print( 29 | \\{{"jsonrpc": "2.0", "method": "{s}", "params": {s}, "id": "{s}"}} 30 | , .{method, params, id.str}), 31 | .null => try writer.print( 32 | \\{{"jsonrpc": "2.0", "method": "{s}", "params": {s}, "id": null}} 33 | , .{method, params}), 34 | .none => try writer.print( 35 | \\{{"jsonrpc": "2.0", "method": "{s}", "params": {s}}} 36 | , .{method, params}), 37 | } 38 | } else { 39 | switch (id) { 40 | .num => try writer.print( 41 | \\{{"jsonrpc": "2.0", "method": "{s}", "id": {}}} 42 | , .{method, id.num}), 43 | .str => try writer.print( 44 | \\{{"jsonrpc": "2.0", "method": "{s}", "id": "{s}"}} 45 | , .{method, id.str}), 46 | .null => try writer.print( 47 | \\{{"jsonrpc": "2.0", "method": "{s}", "id": null}} 48 | , .{method}), 49 | .none => try writer.print( 50 | \\{{"jsonrpc": "2.0", "method": "{s}"}} 51 | , .{method}), 52 | } 53 | } 54 | } 55 | 56 | /// Write a JSON request built with the method, params, and id to the writer. 57 | pub fn writeRequest(alloc: Allocator, method: []const u8, params: anytype, id: RpcId, 58 | writer: *std.Io.Writer) JrErrors!void { 59 | const pinfo = @typeInfo(@TypeOf(params)); 60 | if (pinfo != .array and pinfo != .@"struct" and pinfo != .null) { 61 | return JrErrors.InvalidParamsType; 62 | } 63 | 64 | if (pinfo != .null) { 65 | // TODO: direct write to writer, without output to string. 66 | const params_json = try std.json.Stringify.valueAlloc(alloc, params, .{ 67 | .emit_null_optional_fields = false, 68 | .emit_nonportable_numbers_as_strings = true, 69 | }); 70 | defer alloc.free(params_json); 71 | try writeRequestJson(method, params_json, id, writer); 72 | } else { 73 | try writeRequestJson(method, null, id, writer); 74 | } 75 | } 76 | 77 | /// Build a request message in JSON string. 78 | /// Caller needs to call alloc.free() on the returned message to free the memory. 79 | pub fn makeRequestJson(alloc: Allocator, method: []const u8, params: anytype, 80 | id: RpcId) JrErrors![]const u8 { 81 | const pinfo = @typeInfo(@TypeOf(params)); 82 | if (pinfo != .array and pinfo != .@"struct" and pinfo != .null) { 83 | return JrErrors.InvalidParamsType; 84 | } 85 | 86 | var output_buf = std.Io.Writer.Allocating.init(alloc); 87 | if (pinfo != .null) { 88 | const params_json = try std.json.Stringify.valueAlloc(alloc, params, .{ 89 | .emit_null_optional_fields = false, 90 | .emit_nonportable_numbers_as_strings = true, 91 | }); 92 | defer alloc.free(params_json); 93 | try writeRequestJson(method, params_json, id, &output_buf.writer); 94 | } else { 95 | try writeRequestJson(method, null, id, &output_buf.writer); 96 | } 97 | return try output_buf.toOwnedSlice(); 98 | } 99 | 100 | /// Write a batch message of request JSONS to the writer. 101 | pub fn writeBatchRequestJson(request_jsons: []const []const u8, 102 | writer: *std.Io.Writer) std.Io.Writer.Error!void { 103 | var count: usize = 0; 104 | try writer.writeAll("["); 105 | for (request_jsons) |json| { 106 | if (count > 0) try writer.writeAll(", "); 107 | try writer.writeAll(json); 108 | count += 1; 109 | } 110 | try writer.writeAll("]"); 111 | } 112 | 113 | /// Build a batch message of request JSONS. 114 | /// Caller needs to call alloc.free() on the returned message to free the memory. 115 | /// TODO: remove use Writer.Allocating with writeBatchRequestJson(). 116 | pub fn makeBatchRequestJson(alloc: Allocator, request_jsons: []const []const u8) WriteAllocError![]const u8 { 117 | var output_buf = std.Io.Writer.Allocating.init(alloc); 118 | try writeBatchRequestJson(request_jsons, &output_buf.writer); 119 | return try output_buf.toOwnedSlice(); 120 | } 121 | 122 | 123 | /// Write a normal response message in JSON to the writer. 124 | /// Return true for valid response. For message id that shouldn't have a response, false is returned. 125 | pub fn writeResponseJson(id: RpcId, result_json: []const u8, 126 | writer: *std.Io.Writer) std.Io.Writer.Error!void { 127 | switch (id) { 128 | .num => try writer.print( 129 | \\{{"jsonrpc": "2.0", "result": {s}, "id": {}}} 130 | , .{result_json, id.num}), 131 | .str => try writer.print( 132 | \\{{"jsonrpc": "2.0", "result": {s}, "id": "{s}"}} 133 | , .{result_json, id.str}), 134 | else => unreachable, // Response must have an ID. 135 | } 136 | } 137 | 138 | /// Build a normal response message in JSON string. 139 | /// For message id that shouldn't have a response, null is returned. 140 | /// Caller needs to call alloc.free() on the returned message to free the memory. 141 | pub fn makeResponseJson(alloc: Allocator, id: RpcId, result_json: []const u8) WriteAllocError!?[]const u8 { 142 | if (id.isNotification()) 143 | return null; 144 | var output_buf = std.Io.Writer.Allocating.init(alloc); 145 | try writeResponseJson(id, result_json, &output_buf.writer); 146 | return try output_buf.toOwnedSlice(); 147 | } 148 | 149 | /// Writer an error response message in JSON to the writer. 150 | pub fn writeErrorResponseJson(id: RpcId, err_code: ErrorCode, msg: []const u8, 151 | writer: *std.Io.Writer) std.Io.Writer.Error!void { 152 | const code: i32 = @intFromEnum(err_code); 153 | const err_msg = if (msg.len == 0) @tagName(err_code) else msg; 154 | switch (id) { 155 | .num => try writer.print( 156 | \\{{"jsonrpc": "2.0", "id": {}, "error": {{"code": {}, "message": "{s}"}}}} 157 | , .{id.num, code, err_msg}), 158 | .str => try writer.print( 159 | \\{{"jsonrpc": "2.0", "id": "{s}", "error": {{"code": {}, "message": "{s}"}}}} 160 | , .{id.str, code, err_msg}), 161 | .none => try writer.print( 162 | \\{{"jsonrpc": "2.0", "id": null, "error": {{"code": {}, "message": "{s}"}}}} 163 | , .{code, err_msg}), 164 | .null => try writer.print( 165 | \\{{"jsonrpc": "2.0", "id": null, "error": {{"code": {}, "message": "{s}"}}}} 166 | , .{code, err_msg}), 167 | } 168 | } 169 | 170 | /// Build an error response message in JSON string. 171 | /// Caller needs to call alloc.free() on the returned message to free the memory. 172 | pub fn makeErrorResponseJson(alloc: Allocator, id: RpcId, err_code: ErrorCode, 173 | msg: []const u8) WriteAllocError![]const u8 { 174 | var output_buf = std.Io.Writer.Allocating.init(alloc); 175 | try writeErrorResponseJson(id, err_code, msg, &output_buf.writer); 176 | return try output_buf.toOwnedSlice(); 177 | } 178 | 179 | /// Build an error response message in JSON, with the error data field set. 180 | /// Caller needs to call alloc.free() on the returned message to free the memory. 181 | pub fn writeErrorDataResponseJson(id: RpcId, err_code: ErrorCode, msg: []const u8, 182 | data: []const u8, writer: *std.Io.Writer) std.Io.Writer.Error!void { 183 | const code: i32 = @intFromEnum(err_code); 184 | switch (id) { 185 | .num => try writer.print( 186 | \\{{"jsonrpc": "2.0", "id": {}, "error": {{"code": {}, "message": "{s}", "data": {s}}}}} 187 | , .{id.num, code, msg, data}), 188 | .str => try writer.print( 189 | \\{{"jsonrpc": "2.0", "id": "{s}", "error": {{"code": {}, "message": "{s}", "data": {s}}}}} 190 | , .{id.str, code, msg, data}), 191 | .none => try writer.print( 192 | \\{{"jsonrpc": "2.0", "id": null, "error": {{"code": {}, "message": "{s}", "data": {s}}}}} 193 | , .{code, msg, data}), 194 | .null => try writer.print( 195 | \\{{"jsonrpc": "2.0", "id": null, "error": {{"code": {}, "message": "{s}", "data": {s}}}}} 196 | , .{code, msg, data}), 197 | } 198 | } 199 | 200 | /// Build an error response message in JSON, with the error data field set. 201 | /// Caller needs to call alloc.free() on the returned message to free the memory. 202 | pub fn makeErrorDataResponseJson(alloc: Allocator, id: RpcId, err_code: ErrorCode, 203 | msg: []const u8, data: []const u8) WriteAllocError![]const u8 { 204 | var output_buf = std.Io.Writer.Allocating.init(alloc); 205 | try writeErrorDataResponseJson(id, err_code, msg, data, &output_buf.writer); 206 | return try output_buf.toOwnedSlice(); 207 | } 208 | 209 | 210 | -------------------------------------------------------------------------------- /src/streaming/frame.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const std = @import("std"); 10 | const Allocator = std.mem.Allocator; 11 | const allocPrint = std.fmt.allocPrint; 12 | const ArrayList = std.ArrayList; 13 | const StringHashMap = std.hash_map.StringHashMap; 14 | const RpcId = @import("../jsonrpc/request.zig").RpcId; 15 | const makeRequestJson = @import("../jsonrpc/composer.zig").makeRequestJson; 16 | const makeResponseJson = @import("../jsonrpc/composer.zig").makeResponseJson; 17 | const JrErrors = @import("../zigjr.zig").JrErrors; 18 | const BufReader = @import("BufReader.zig"); 19 | 20 | 21 | // Trim header key and value by these characters. 22 | const TRIM_SET = " \t\r\n"; 23 | 24 | // Header key and value positions in the FrameData.buf. 25 | const Pos = [4]usize; 26 | 27 | /// The header the content data of a request frame. 28 | pub const FrameData = struct { 29 | alloc: Allocator, 30 | buf: std.Io.Writer.Allocating, // The header and content data of the whole frame. 31 | http_method: []const u8 = "", 32 | http_path: []const u8 = "", 33 | http_version: []const u8 = "", 34 | header_pos: ArrayList(Pos), 35 | content_start: usize, 36 | content_length: ?usize = null, 37 | 38 | pub fn init(alloc: Allocator) @This() { 39 | return .{ 40 | .alloc = alloc, 41 | .header_pos = .empty, 42 | .buf = .init(alloc), 43 | .content_start = 0, 44 | }; 45 | } 46 | 47 | pub fn deinit(self: *FrameData) void { 48 | self.header_pos.deinit(self.alloc); 49 | self.buf.deinit(); 50 | } 51 | 52 | pub fn reset(self: *FrameData) void { 53 | self.header_pos.clearRetainingCapacity(); 54 | self.buf.clearRetainingCapacity(); 55 | self.content_start = 0; 56 | } 57 | 58 | fn bufWriter(self: *FrameData) *std.Io.Writer { 59 | return &self.buf.writer; 60 | } 61 | 62 | fn bufData(self: *FrameData) []const u8 { 63 | return self.buf.written(); 64 | } 65 | 66 | fn currentPos(self: *FrameData) usize { 67 | return self.bufData().len; 68 | } 69 | 70 | fn addHeader(self: *FrameData, key_start: usize, key_end: usize, 71 | value_start: usize, value_end: usize) !void { 72 | try self.header_pos.append(self.alloc, .{ key_start, key_end, value_start, value_end }); 73 | } 74 | 75 | pub fn headerCount(self: *const FrameData) usize { 76 | return self.header_pos.items.len; 77 | } 78 | 79 | /// Get the header key at 'idx'. 80 | /// Slice might get invalidated if buf is grown while the frame is being read. 81 | pub fn headerKey(self: *FrameData, idx: usize) []const u8 { 82 | const start = self.header_pos.items[idx][0]; 83 | const end = self.header_pos.items[idx][1]; 84 | return std.mem.trim(u8, self.bufData()[start..end], TRIM_SET); 85 | } 86 | 87 | /// Get the header key at 'idx'. 88 | /// Slice might get invalidated if buf is grown while the frame is being read. 89 | pub fn headerValue(self: *FrameData, idx: usize) []const u8 { 90 | const start = self.header_pos.items[idx][2]; 91 | const end = self.header_pos.items[idx][3]; 92 | return std.mem.trim(u8, self.bufData()[start..end], TRIM_SET); 93 | } 94 | 95 | /// Get the header key at 'idx'. 96 | /// Slice might get invalidated if buf is grown while the frame is being read. 97 | pub fn findHeader(self: *FrameData, key: []const u8) ?[]const u8 { 98 | for (0..self.headerCount())|idx| { 99 | if (std.mem.eql(u8, key, self.headerKey(idx))) { 100 | return self.headerValue(idx); 101 | } 102 | } 103 | return null; 104 | } 105 | 106 | fn setupContent(self: *FrameData) !void { 107 | self.content_start = self.currentPos(); 108 | self.content_length = if (self.findHeader("Content-Length")) |value| 109 | try std.fmt.parseInt(usize, value, 10) 110 | else null; 111 | } 112 | 113 | pub fn getContent(self: *FrameData) []const u8 { 114 | return self.bufData()[self.content_start..]; 115 | } 116 | 117 | }; 118 | 119 | /// Read the HTTP request line. 120 | /// e.g. GET /index.html HTTP/1.1\r\n 121 | pub fn readHttpLine(reader: *std.Io.Reader, frame: *FrameData) !void { 122 | const start_pos = frame.currentPos(); 123 | const read_len = try reader.streamDelimiter(frame.bufWriter(), '\n'); 124 | const end_pos = frame.currentPos(); 125 | reader.toss(1); 126 | if (read_len == 0) return; 127 | const line = frame.bufData()[start_pos..end_pos]; 128 | const trimmed = std.mem.trim(u8, line, "\r\n"); 129 | var tokens = std.mem.tokenizeAny(u8, trimmed, " "); 130 | frame.http_method = tokens.next() orelse ""; 131 | frame.http_path = tokens.next() orelse ""; 132 | frame.http_version = tokens.next() orelse ""; 133 | } 134 | 135 | /// Read the HTTP-style headers of a data frame. 136 | /// Content not read yet after this call. Use `readContentLengthFrame()` instead. 137 | /// The data frame has the format of: 138 | /// Content-Length: DATA_LENGTH\r\n 139 | /// Other-Header: VALUE\r\n 140 | /// ... 141 | /// \r\n 142 | /// CONTENT DATA 143 | /// Caller checks frame_data.headerCount() for headers read. 144 | pub fn readHttpHeaders(reader: *std.Io.Reader, frame_data: *FrameData) !void { 145 | while (true) { 146 | const start_pos = frame_data.currentPos(); 147 | const read_len = try reader.streamDelimiter(frame_data.bufWriter(), '\n'); 148 | const end_pos = frame_data.currentPos(); 149 | reader.toss(1); // skip the '\n' char in reader. 150 | if (read_len == 0) 151 | break; // reach an empty line '\n'; end of headers. 152 | const line = frame_data.bufData()[start_pos..end_pos]; 153 | const trimmed = std.mem.trim(u8, line, "\r\n"); 154 | if (trimmed.len == 0) { // reach an empty line "\r\n"; end of headers. 155 | break; // caller checks frame_data.headerCount() for headers. 156 | } 157 | const colon_pos = if (std.mem.indexOfScalar(u8, line, ':')) |pos| pos else 0; 158 | if (colon_pos == 0) 159 | continue; // missing ':" or empty key. 160 | const key_start = start_pos; 161 | const key_end = start_pos + colon_pos; 162 | const val_start = start_pos + colon_pos + 1; 163 | const val_end = end_pos; // empty value is acceptable. 164 | try frame_data.addHeader(key_start, key_end, val_start, val_end); 165 | } 166 | try frame_data.setupContent(); 167 | } 168 | 169 | /// Read a data frame, that has a Content-Length header, into frame_data. 170 | /// The headers and the content data are kept in the frame_data. 171 | pub fn readContentLengthFrame(reader: *std.Io.Reader, frame_data: *FrameData) !bool { 172 | readHttpHeaders(reader, frame_data) catch |err| { 173 | if (err == error.EndOfStream) 174 | return false; // no more data. 175 | return err; // unrecoverable error while reading from reader. 176 | }; 177 | if (frame_data.content_length) |len| { 178 | _ = try reader.stream(frame_data.bufWriter(), .limited(len)); 179 | return true; // has content data. 180 | } else { 181 | return JrErrors.MissingContentLengthHeader; 182 | } 183 | } 184 | 185 | 186 | pub fn writeHttpStatusLine(writer: *std.Io.Writer, http_version: []const u8, 187 | status_code: usize, status: []const u8) !void { 188 | try writer.print("HTTP/{s} {d} {s}\r\n", .{http_version, status_code, status}); 189 | } 190 | 191 | /// Write a data frame to a writer, with a header section containing 192 | /// the Content-Length header for the data. 193 | pub fn writeContentLengthFrame(writer: *std.Io.Writer, content: []const u8) !void { 194 | try writer.print("Content-Length: {d}\r\n\r\n", .{content.len}); 195 | try writer.writeAll(content); 196 | } 197 | 198 | /// Write a sequence of data frames into a writer, 199 | /// where each frame with a header section containing the Content-Length header. 200 | pub fn writeContentLengthFrames(writer: *std.Io.Writer, frame_contents: []const []const u8) !void { 201 | for (frame_contents)|content| 202 | try writeContentLengthFrame(writer, content); 203 | } 204 | 205 | /// Write a sequence of data frames into a writer, 206 | /// where each frame with a header section containing the Content-Length header. 207 | pub fn allocContentLengthFrames(alloc: Allocator, frame_contents: []const []const u8) !std.Io.Writer.Allocating { 208 | var alloc_writer = std.Io.Writer.Allocating.init(alloc); 209 | try writeContentLengthFrames(&alloc_writer.writer, frame_contents); 210 | return alloc_writer; 211 | } 212 | 213 | 214 | /// Write a request data frame to a writer, with a header section containing 215 | /// the Content-Length header for the data. 216 | pub fn writeContentLengthRequest(alloc: Allocator, writer: *std.Io.Writer, 217 | method: []const u8, params: anytype, id: RpcId) !void { 218 | const json = try makeRequestJson(alloc, method, params, id); 219 | defer alloc.free(json); 220 | try writeContentLengthFrame(writer, json); 221 | } 222 | 223 | /// Write a request data frame to a writer, with a header section containing 224 | /// the Content-Length header for the data. 225 | pub fn writeContentLengthResponse(alloc: Allocator, writer: *std.Io.Writer, 226 | result_json: []const u8, id: RpcId) !void { 227 | const json = try makeResponseJson(alloc, id, result_json); 228 | defer alloc.free(json); 229 | try writeContentLengthFrame(writer, json); 230 | } 231 | 232 | 233 | -------------------------------------------------------------------------------- /examples/hello_net.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const std = @import("std"); 10 | const Allocator = std.mem.Allocator; 11 | const ParseOptions = std.json.ParseOptions; 12 | const StringHashMap = std.hash_map.StringHashMap; 13 | 14 | const zigjr = @import("zigjr"); 15 | 16 | 17 | // For --http, test with 18 | // curl localhost:35354 --request POST --json @data/hello.json 19 | // curl localhost:35354 --request POST --json @data/hello_name.json 20 | 21 | // For --tcp, test with 22 | // nc64 localhost 35354 < data\hello.json 23 | // nc64 localhost 35354 < data\hello_name.json 24 | 25 | 26 | pub fn main() !void { 27 | var gpa = std.heap.DebugAllocator(.{}){}; 28 | defer _ = gpa.deinit(); 29 | const alloc = gpa.allocator(); 30 | var args = try CmdArgs.init(alloc); 31 | defer args.deinit(); 32 | args.parse() catch { usage(); return; }; 33 | 34 | const run_http = if (args.is_http) true else if (args.is_tcp) false 35 | else { 36 | usage(); 37 | return; 38 | }; 39 | 40 | const listen_address = try std.fmt.allocPrint(alloc, "0.0.0.0:{s}", .{args.port}); 41 | defer alloc.free(listen_address); 42 | const local_address = try std.fmt.allocPrint(alloc, "127.0.0.1:{s}", .{args.port}); 43 | defer alloc.free(local_address); 44 | std.debug.print("Server listening at port: {s}\n", .{args.port}); 45 | 46 | var ctx = ServerCtx { 47 | .is_http = run_http, 48 | .listen_address = listen_address, 49 | .local_address = local_address, 50 | .end_server = std.atomic.Value(bool).init(false), 51 | }; 52 | 53 | const rpc_dispatcher = try createDispatcher(alloc, &ctx); 54 | defer { 55 | rpc_dispatcher.deinit(); 56 | alloc.destroy(rpc_dispatcher); 57 | } 58 | const dispatcher = zigjr.RequestDispatcher.implBy(rpc_dispatcher); 59 | try runServer(&ctx, dispatcher); 60 | } 61 | 62 | const ServerCtx = struct { 63 | is_http: bool, 64 | listen_address: []const u8, 65 | local_address: []const u8, 66 | end_server: std.atomic.Value(bool), 67 | }; 68 | 69 | fn createDispatcher(alloc: Allocator, ctx: *ServerCtx) !*zigjr.RpcDispatcher { 70 | var rpc_dispatcher = try alloc.create(zigjr.RpcDispatcher); 71 | rpc_dispatcher.* = try zigjr.RpcDispatcher.init(alloc); 72 | 73 | try rpc_dispatcher.add("hello", hello); 74 | try rpc_dispatcher.add("hello-name", helloName); 75 | try rpc_dispatcher.add("hello-xtimes", helloXTimes); 76 | try rpc_dispatcher.add("substr", substr); 77 | try rpc_dispatcher.add("say", say); 78 | try rpc_dispatcher.add("opt-text", optionalText); 79 | try rpc_dispatcher.add("end-session", endSession); 80 | try rpc_dispatcher.addWithCtx("end-server", ctx, endServer); 81 | 82 | return rpc_dispatcher; 83 | } 84 | 85 | fn runServer(ctx: *ServerCtx, dispatcher: zigjr.RequestDispatcher) !void { 86 | const net_addr = try std.net.Address.parseIpAndPort(ctx.listen_address); 87 | var server = try net_addr.listen(.{ .reuse_address = true }); 88 | defer server.deinit(); 89 | 90 | while (true) { 91 | if (ctx.end_server.load(std.builtin.AtomicOrder.seq_cst)) 92 | break; 93 | const connection = try server.accept(); 94 | if (ctx.end_server.load(std.builtin.AtomicOrder.seq_cst)) 95 | break; 96 | 97 | if (ctx.is_http) { 98 | _ = try std.Thread.spawn(.{}, httpWorker, .{dispatcher, connection}); 99 | } else { 100 | _ = try std.Thread.spawn(.{}, netWorker, .{dispatcher, connection}); 101 | } 102 | } 103 | } 104 | 105 | fn httpWorker(dispatcher: zigjr.RequestDispatcher, connection: std.net.Server.Connection) void { 106 | std.debug.print("Start HTTP session.\n", .{}); 107 | 108 | var gpa = std.heap.DebugAllocator(.{}){}; 109 | defer _ = gpa.deinit(); 110 | const alloc = gpa.allocator(); 111 | 112 | runHttpSession(alloc, dispatcher, connection) catch |e| { 113 | std.debug.print("Error in HTTP session: {any}\n", .{e}); 114 | }; 115 | 116 | connection.stream.close(); 117 | std.debug.print("End HTTP session.\n", .{}); 118 | } 119 | 120 | fn netWorker(dispatcher: zigjr.RequestDispatcher, connection: std.net.Server.Connection) void { 121 | std.debug.print("Start session (netWorker).\n", .{}); 122 | 123 | var gpa = std.heap.DebugAllocator(.{}){}; 124 | defer _ = gpa.deinit(); 125 | const alloc = gpa.allocator(); 126 | 127 | runNetSession(alloc, dispatcher, connection) catch |e| { 128 | std.debug.print("Error in runSession: {any}\n", .{e}); 129 | }; 130 | 131 | connection.stream.close(); 132 | std.debug.print("End session (netWorker).\n", .{}); 133 | } 134 | 135 | fn runHttpSession(alloc: Allocator, dispatcher: zigjr.RequestDispatcher, 136 | connection: std.net.Server.Connection) !void { 137 | var rbuf: [1024]u8 = undefined; 138 | var wbuf: [1024]u8 = undefined; 139 | var s_reader = connection.stream.reader(&rbuf); 140 | var s_writer = connection.stream.writer(&wbuf); 141 | const reader = s_reader.interface(); 142 | const writer = &s_writer.interface; 143 | var dbg_logger = zigjr.DbgLogger{}; 144 | var pipeline = try zigjr.RequestPipeline.init(alloc, dispatcher, dbg_logger.asLogger()); 145 | var frame = zigjr.frame.FrameData.init(alloc); 146 | 147 | defer pipeline.deinit(); 148 | defer frame.deinit(); 149 | 150 | try zigjr.frame.readHttpLine(reader, &frame); 151 | std.debug.print("HTTP Line: {s} {s} {s}\n", .{ frame.http_method, frame.http_path, frame.http_version }); 152 | const has_data = try zigjr.frame.readContentLengthFrame(reader, &frame); 153 | if (!has_data) 154 | return; 155 | 156 | const request_json = std.mem.trim(u8, frame.getContent(), " \t\n\r"); 157 | // std.debug.print("content_length: {any}, request_json: |{s}|\n", .{ frame.content_length, request_json }); 158 | 159 | const run_status = try pipeline.runRequest(request_json); 160 | if (run_status.hasReply()) { 161 | try zigjr.frame.writeHttpStatusLine(writer, "1.1", 200, "OK"); // HTTP/1.1 200 OK\r\n 162 | try zigjr.frame.writeContentLengthFrame(writer, pipeline.responseJson()); 163 | try writer.flush(); 164 | } else { 165 | std.debug.print("No response\n", .{}); 166 | } 167 | } 168 | 169 | fn runNetSession(alloc: Allocator, dispatcher: zigjr.RequestDispatcher, 170 | connection: std.net.Server.Connection) !void { 171 | var rbuf: [1024]u8 = undefined; 172 | var wbuf: [1024]u8 = undefined; 173 | var s_reader = connection.stream.reader(&rbuf); 174 | var s_writer = connection.stream.writer(&wbuf); 175 | const reader = s_reader.interface(); 176 | const writer = &s_writer.interface; 177 | var dbg_logger = zigjr.DbgLogger{}; 178 | 179 | zigjr.stream.requestsByDelimiter(alloc, reader, writer, dispatcher, .{ 180 | .logger = dbg_logger.asLogger() 181 | }) catch |e| { 182 | if (e == error.ReadFailed) return else return e; 183 | }; 184 | } 185 | 186 | fn touchServer(address: []const u8) !void { 187 | const net_addr = try std.net.Address.parseIpAndPort(address); 188 | var stream = try std.net.tcpConnectToAddress(net_addr); 189 | defer stream.close(); 190 | } 191 | 192 | // A handler with no parameter and returns a string. 193 | fn hello() []const u8 { 194 | return "Hello world"; 195 | } 196 | 197 | // A handler takes in a string parameter and returns a string with error. 198 | // It also asks the library for an allocator, which is passed in automatically. 199 | // Allocated memory is freed automatically, making memory usage simple. 200 | fn helloName(dc: *zigjr.DispatchCtx, name: [] const u8) Allocator.Error![]const u8 { 201 | return try std.fmt.allocPrint(dc.arena(), "Hello {s}", .{name}); 202 | } 203 | 204 | // This one takes one more parameter. Note that i64 is JSON's integer type. 205 | fn helloXTimes(dc: *zigjr.DispatchCtx, name: [] const u8, times: i64) ![]const u8 { 206 | const repeat: usize = if (0 < times and times < 100) @intCast(times) else 1; 207 | var buf = std.Io.Writer.Allocating.init(dc.arena()); 208 | for (0..repeat) |_| try buf.writer.print("Hello {s}! ", .{name}); 209 | return buf.written(); 210 | } 211 | 212 | fn substr(name: [] const u8, start: i64, len: i64) []const u8 { 213 | return name[@intCast(start) .. @intCast(len)]; 214 | } 215 | 216 | // A handler takes in a string and has no return value, for RPC notification. 217 | fn say(msg: [] const u8) void { 218 | std.debug.print("Message to say: {s}\n", .{msg}); 219 | } 220 | 221 | fn optionalText(text: ?[] const u8) []const u8 { 222 | if (text)|txt| { 223 | return txt; 224 | } else { 225 | return "No text"; 226 | } 227 | } 228 | 229 | fn endSession() zigjr.DispatchResult { 230 | return zigjr.DispatchResult.asEndStream(); 231 | } 232 | 233 | fn endServer(ctx: *ServerCtx) zigjr.DispatchResult { 234 | // Set server termination flag. 235 | ctx.end_server.store(true, std.builtin.AtomicOrder.seq_cst); 236 | // Need to wake up server blocking at the .accept() call. 237 | touchServer(ctx.local_address) catch |e| { 238 | std.debug.print("Error in touching server at {s}. Error: {any}\n", .{ctx.local_address, e}); 239 | }; 240 | return zigjr.DispatchResult.asEndStream(); 241 | } 242 | 243 | 244 | fn usage() void { 245 | std.debug.print( 246 | \\Usage: hello_net [--http | --tcp] [--port N] 247 | \\ 248 | \\ --http - runs the server over HTTP. JSON-RPC messages using Content-Length headers. 249 | \\ --tcp - runs the server over plain TCP. JSON-RPC messages using LF delimiters. 250 | \\ --port N - set the listening port of the server. 251 | \\ 252 | , .{}); 253 | } 254 | 255 | 256 | const CmdArgs = struct { 257 | const Self = @This(); 258 | 259 | arg_itr: std.process.ArgIterator, 260 | is_http: bool = false, 261 | is_tcp: bool = false, 262 | port: []const u8 = "35354", 263 | 264 | fn init(alloc: Allocator) !CmdArgs { 265 | var args = CmdArgs { .arg_itr = try std.process.argsWithAllocator(alloc) }; 266 | _ = args.arg_itr.next(); 267 | return args; 268 | } 269 | 270 | fn deinit(self: *CmdArgs) void { 271 | self.arg_itr.deinit(); 272 | } 273 | 274 | fn parse(self: *Self) !void { 275 | var argv = self.arg_itr; 276 | while (argv.next())|argz| { 277 | const arg = std.mem.sliceTo(argz, 0); 278 | if (std.mem.eql(u8, arg, "--http")) { 279 | self.is_http = true; 280 | } else if (std.mem.eql(u8, arg, "--tcp")) { 281 | self.is_tcp = true; 282 | } else if (std.mem.eql(u8, arg, "--port")) { 283 | if (argv.next())|argz1| { 284 | self.port = std.mem.sliceTo(argz1, 0); 285 | } 286 | } 287 | } 288 | } 289 | 290 | }; 291 | 292 | 293 | -------------------------------------------------------------------------------- /src/jsonrpc/request.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const std = @import("std"); 10 | const Type = std.builtin.Type; 11 | const Allocator = std.mem.Allocator; 12 | const Parsed = std.json.Parsed; 13 | const Scanner = std.json.Scanner; 14 | const ParseOptions = std.json.ParseOptions; 15 | const innerParse = std.json.innerParse; 16 | const ParseError = std.json.ParseError; 17 | const Value = std.json.Value; 18 | const Array = std.json.Array; 19 | const ObjectMap = std.json.ObjectMap; 20 | 21 | const errors = @import("errors.zig"); 22 | const ErrorCode = errors.ErrorCode; 23 | const JrErrors = errors.JrErrors; 24 | const Owned = @import("../rpc/deiniter.zig").Owned; 25 | 26 | 27 | /// Parse request_json into a RpcRequestResult. 28 | /// Caller manages the lifetime request_json. Needs to ensure request_json is not 29 | /// freed before RpcRequestResult.deinit(). Parsed result references request_json. 30 | pub fn parseRpcRequest(alloc: Allocator, request_json: []const u8) RpcRequestResult { 31 | return parseRpcRequestOpts(alloc, request_json, .{ 32 | .ignore_unknown_fields = true, 33 | }); 34 | } 35 | 36 | /// Parse request_json into a RpcRequestResult. 37 | /// Caller transfers ownership of request_json to RpcRequestResult. 38 | /// They will be freed in the RpcRequestResult.deinit(). 39 | /// This allows managing the lifetime of request_json and result together. 40 | pub fn parseRpcRequestOwned(alloc: Allocator, request_json: []const u8, opts: ParseOptions) RpcRequestResult { 41 | var rresult = parseRpcRequestOpts(alloc, request_json, opts); 42 | rresult.jsonOwned(request_json, alloc); 43 | return rresult; 44 | } 45 | 46 | pub fn parseRpcRequestOpts(alloc: Allocator, request_json: []const u8, opts: ParseOptions) RpcRequestResult { 47 | const parsed = std.json.parseFromSlice(RpcRequestMessage, alloc, request_json, opts) catch |err| { 48 | // Return an empty request with the error so callers can have a uniform request handling. 49 | return .{ 50 | .request_msg = .{ .request = RpcRequest.ofParseErr(err) } 51 | }; 52 | }; 53 | return .{ 54 | .request_msg = parsed.value, 55 | ._parsed = parsed, 56 | }; 57 | } 58 | 59 | pub const RpcRequestResult = struct { 60 | const Self = @This(); 61 | request_msg: RpcRequestMessage, 62 | _parsed: ?std.json.Parsed(RpcRequestMessage) = null, 63 | _request_json: Owned([]const u8) = .{}, 64 | 65 | pub fn deinit(self: *Self) void { 66 | if (self._parsed) |parsed| parsed.deinit(); 67 | self._request_json.deinit(); 68 | } 69 | 70 | fn jsonOwned(self: *Self, request_json: []const u8, alloc: Allocator) void { 71 | self._request_json = Owned([]const u8).init(request_json, alloc); 72 | } 73 | 74 | pub fn isRequest(self: Self) bool { 75 | return self.request_msg == .request; 76 | } 77 | 78 | pub fn isBatch(self: Self) bool { 79 | return self.request_msg == .batch; 80 | } 81 | 82 | pub fn isMissingMethod(self: Self) bool { 83 | if (self.request_msg == .request) { 84 | return self.request_msg.request._no_method; 85 | } else { 86 | return false; 87 | } 88 | } 89 | 90 | /// Shortcut to access the inner tagged union invariant request. 91 | /// Can also access via switch(request_msg) .request => , .batch => 92 | pub fn request(self: *Self) !RpcRequest { 93 | return if (self.isRequest()) self.request_msg.request else JrErrors.NotSingleRpcRequest; 94 | } 95 | 96 | /// Shortcut to access the inner tagged union invariant batch. 97 | pub fn batch(self: *Self) ![]const RpcRequest { 98 | return if (self.isBatch()) self.request_msg.batch else JrErrors.NotBatchRpcRequest; 99 | } 100 | }; 101 | 102 | pub const RpcRequestMessage = union(enum) { 103 | request: RpcRequest, // JSON-RPC's single request 104 | batch: []RpcRequest, // JSON-RPC's batch of requests 105 | 106 | // Custom parsing when the JSON parser encounters a field of this type. 107 | pub fn jsonParse(alloc: Allocator, source: anytype, options: ParseOptions) !RpcRequestMessage { 108 | return switch (try source.peekNextTokenType()) { 109 | .object_begin => { 110 | var req = try innerParse(RpcRequest, alloc, source, options); 111 | req.validate(); 112 | return .{ .request = req }; 113 | }, 114 | .array_begin => { 115 | const batch = try innerParse([]RpcRequest, alloc, source, options); 116 | for (batch)|*req| req.validate(); 117 | return .{ .batch = batch }; 118 | }, 119 | else => error.UnexpectedToken, // there're only two cases; any others are error. 120 | }; 121 | } 122 | }; 123 | 124 | pub const RpcRequest = struct { 125 | const Self = @This(); 126 | 127 | jsonrpc: [3]u8 = .{ '0', '.', '0' }, // default to fail validation. 128 | method: []u8 = "", 129 | params: Value = .{ .null = {} }, // default for optional field. 130 | id: RpcId = .{ .none = {} }, // default for optional field. 131 | _err: RpcRequestError = .{}, // attach parsing error and validation error here. 132 | _no_method: bool = false, // treat MissingField error as no method. 133 | 134 | fn ofParseErr(parse_err: ParseError(Scanner)) Self { 135 | var empty_req = Self{}; 136 | empty_req._err = RpcRequestError.fromParseError(parse_err); 137 | empty_req._no_method = (parse_err == error.MissingField); 138 | return empty_req; 139 | } 140 | 141 | fn validate(self: *Self) void { 142 | if (RpcRequestError.validateRequest(self)) |e| { 143 | self._err = e; 144 | } 145 | } 146 | 147 | pub fn err(self: Self) RpcRequestError { 148 | return self._err; 149 | } 150 | 151 | pub fn hasError(self: Self) bool { 152 | return self.err().code != ErrorCode.None; 153 | } 154 | 155 | pub fn isError(self: Self, code: ErrorCode) bool { 156 | return self.err().code == code; 157 | } 158 | 159 | pub fn hasParams(self: Self) bool { 160 | return self.params != .null; 161 | } 162 | 163 | pub fn hasArrayParams(self: Self) bool { 164 | return self.params == .array; 165 | } 166 | 167 | pub fn hasObjectParams(self: Self) bool { 168 | return self.params == .object; 169 | } 170 | 171 | pub fn arrayParams(self: Self) ?std.json.Array { 172 | return if (self.params == .array) self.params.array else null; 173 | } 174 | 175 | pub fn objectParams(self: Self) ?std.json.ObjectMap { 176 | return if (self.params == .object) self.params.object else null; 177 | } 178 | }; 179 | 180 | pub const RpcId = union(enum) { 181 | none: void, // id is missing (not set at all). 182 | null: void, // id is set to null. 183 | num: i64, 184 | str: []const u8, 185 | 186 | pub fn ofNone() RpcId { return .{ .none = {} }; } 187 | pub fn ofNull() RpcId { return .{ .null = {} }; } 188 | pub fn of(id: i64) RpcId { return .{ .num = id }; } 189 | pub fn ofStr(id: []const u8) RpcId { return .{ .str = id }; } 190 | 191 | pub fn jsonParse(alloc: Allocator, source: anytype, options: ParseOptions) !RpcId { 192 | const value = try innerParse(Value, alloc, source, options); 193 | return switch (value) { 194 | .null => .{ .null = value.null }, 195 | .integer => .{ .num = value.integer }, 196 | .string => .{ .str = value.string }, 197 | else => error.UnexpectedToken, 198 | }; 199 | } 200 | 201 | pub fn isValid(self: @This()) bool { 202 | return self == .num or self == .str; 203 | } 204 | 205 | pub fn isNotification(self: @This()) bool { 206 | return !self.isValid(); 207 | } 208 | 209 | pub fn isNone(self: @This()) bool { 210 | return self == .none; 211 | } 212 | 213 | pub fn isNull(self: @This()) bool { 214 | return self == .null; 215 | } 216 | 217 | pub fn eql(self: @This(), value: anytype) bool { 218 | const value_info = @typeInfo(@TypeOf(value)); 219 | switch (value_info) { 220 | .comptime_int, 221 | .int => return self == .num and self.num == value, 222 | .pointer => { 223 | const element_info = @typeInfo(value_info.pointer.child); 224 | return element_info == .array and element_info.array.child == u8 and 225 | self == .str and std.mem.eql(u8, self.str, value); 226 | }, 227 | .array => { 228 | return value_info.array.child == u8 and 229 | self == .str and std.mem.eql(u8, self.str, &value); 230 | }, 231 | else => @compileError("RpcId value can only be integer or string."), 232 | } 233 | } 234 | 235 | }; 236 | 237 | pub const RpcRequestError = struct { 238 | const Self = @This(); 239 | 240 | code: ErrorCode = ErrorCode.None, 241 | err_msg: []const u8 = "", // only constant string, no allocation. 242 | req_id: RpcId = .{ .null = {} }, // request id related to the error. 243 | 244 | fn fromParseError(parse_err: ParseError(Scanner)) Self { 245 | return switch (parse_err) { 246 | error.MissingField, error.UnknownField, error.DuplicateField, 247 | error.LengthMismatch, error.UnexpectedEndOfInput => .{ 248 | .code = ErrorCode.InvalidRequest, 249 | .err_msg = @errorName(parse_err), 250 | }, 251 | error.Overflow, error.OutOfMemory => .{ 252 | .code = ErrorCode.InternalError, 253 | .err_msg = @errorName(parse_err), 254 | }, 255 | else => .{ 256 | .code = ErrorCode.ParseError, 257 | .err_msg = @errorName(parse_err), 258 | }, 259 | }; 260 | } 261 | 262 | fn validateRequest(body: *RpcRequest) ?Self { 263 | if (!std.mem.eql(u8, &body.jsonrpc, "2.0")) { 264 | return .{ 265 | .code = ErrorCode.InvalidRequest, 266 | .err_msg = "Invalid JSON-RPC version. Must be 2.0.", 267 | .req_id = body.id, 268 | }; 269 | } 270 | if (body.params != .array and body.params != .object and body.params != .null) { 271 | return .{ 272 | .code = ErrorCode.InvalidParams, 273 | .err_msg = "'Params' must be an array, an object, or not defined.", 274 | .req_id = body.id, 275 | }; 276 | } 277 | if (body.method.len == 0) { 278 | return .{ 279 | .code = ErrorCode.InvalidRequest, 280 | .err_msg = "'Method' is empty.", 281 | .req_id = body.id, 282 | }; 283 | } 284 | return null; // return null RpcRequestError for validation passed. 285 | } 286 | 287 | }; 288 | 289 | 290 | -------------------------------------------------------------------------------- /src/streaming/stream.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const std = @import("std"); 10 | const Allocator = std.mem.Allocator; 11 | const allocPrint = std.fmt.allocPrint; 12 | const ArrayList = std.ArrayList; 13 | // const bufferedWriter = std.io.bufferedWriter; 14 | 15 | const zigjr = @import("../zigjr.zig"); 16 | const RpcDispatcher = zigjr.RpcDispatcher; 17 | const RequestDispatcher = zigjr.RequestDispatcher; 18 | const ResponseDispatcher = zigjr.ResponseDispatcher; 19 | const JrErrors = zigjr.JrErrors; 20 | const frame = @import("frame.zig"); 21 | 22 | 23 | const TRIM_SET = " \t\r\n"; 24 | 25 | 26 | // TODO: Remove and update README 27 | /// Runs a loop to read a stream of delimitered JSON request messages (frames) from the reader, 28 | /// handle each one with the RpcDispatcher, and write the JSON responses to the writer. 29 | pub fn runByDelimiter(alloc: Allocator, reader: *std.Io.Reader, writer: *std.Io.Writer, 30 | rpc_dispatcher: *RpcDispatcher, options: DelimiterOptions) !void { 31 | const rpc_dispatcher_ptr = rpc_dispatcher; 32 | const dispatcher = RequestDispatcher.implBy(rpc_dispatcher_ptr); 33 | try requestsByDelimiter(alloc, reader, writer, dispatcher, options); 34 | } 35 | 36 | /// Runs a loop to read a stream of delimitered JSON request messages (frames) from the reader, 37 | /// handle each one with the dispatcher interface, and write the JSON responses to the writer. 38 | pub fn requestsByDelimiter(alloc: Allocator, reader: *std.Io.Reader, writer: *std.Io.Writer, 39 | dispatcher: RequestDispatcher, options: DelimiterOptions) !void { 40 | var frame_buf = std.Io.Writer.Allocating.init(alloc); 41 | defer frame_buf.deinit(); 42 | 43 | var pipeline = try zigjr.RequestPipeline.init(alloc, dispatcher, options.logger); 44 | defer pipeline.deinit(); 45 | 46 | options.logger.start("[stream.requestsByDelimiter] Logging starts"); 47 | defer { options.logger.stop("[stream.requestsByDelimiter] Logging stops"); } 48 | 49 | while (true) { 50 | frame_buf.clearRetainingCapacity(); 51 | _ = reader.streamDelimiter(&frame_buf.writer, options.request_delimiter) catch |e| { 52 | switch (e) { 53 | error.EndOfStream => break, 54 | else => return e, // unrecoverable error while reading from reader. 55 | } 56 | }; 57 | reader.toss(1); // skip the delimiter char in reader. 58 | 59 | const request_json = std.mem.trim(u8, frame_buf.written(), TRIM_SET); 60 | if (options.skip_blank_message and request_json.len == 0) continue; 61 | 62 | const run_status = try pipeline.runRequest(request_json); 63 | if (run_status.hasReply()) { 64 | try writer.writeAll(pipeline.responseJson()); 65 | try writer.writeByte(options.response_delimiter); 66 | try writer.flush(); 67 | } 68 | if (run_status.end_stream) { 69 | break; 70 | } 71 | } 72 | } 73 | 74 | 75 | /// Runs a loop to read a stream of JSON response messages (frames) from the reader, 76 | /// and handle each one with the dispatcher. 77 | pub fn responsesByDelimiter(alloc: Allocator, reader: *std.Io.Reader, 78 | dispatcher: ResponseDispatcher, options: DelimiterOptions) !void { 79 | var frame_buf = std.Io.Writer.Allocating.init(alloc); 80 | defer frame_buf.deinit(); 81 | 82 | var pipeline = try zigjr.ResponsePipeline.init(alloc, dispatcher); 83 | defer pipeline.deinit(); 84 | 85 | options.logger.start("[stream.responsesByDelimiter] Logging starts"); 86 | defer { options.logger.stop("[stream.responsesByDelimiter] Logging stops"); } 87 | 88 | while (true) { 89 | frame_buf.clearRetainingCapacity(); 90 | _ = reader.streamDelimiter(&frame_buf.writer, options.request_delimiter) catch |e| { 91 | switch (e) { 92 | error.EndOfStream => break, 93 | else => return e, // unrecoverable error while reading from reader. 94 | } 95 | }; 96 | reader.toss(1); // skip the delimiter char in reader. 97 | 98 | const response_json = std.mem.trim(u8, frame_buf.written(), TRIM_SET); 99 | if (options.skip_blank_message and response_json.len == 0) continue; 100 | 101 | options.logger.log("stream.responsesByDelimiter", "receive response", response_json); 102 | const run_status = pipeline.runResponse(response_json, null) catch |err| { 103 | var stderr_writer = std.fs.File.stderr().writer(&.{}); 104 | const stderr = &stderr_writer.interface; 105 | stderr.print("Error in runResponse(). {any}", .{err}) catch {}; 106 | continue; 107 | }; 108 | if (run_status.end_stream) { 109 | break; 110 | } 111 | 112 | } 113 | } 114 | 115 | pub const DelimiterOptions = struct { 116 | request_delimiter: u8 = '\n', 117 | response_delimiter: u8 = '\n', 118 | skip_blank_message: bool = true, 119 | logger: zigjr.Logger = zigjr.Logger.implBy(&nopLogger), 120 | }; 121 | 122 | 123 | /// Runs a loop to read a stream of Content-length based JSON request messages (frames) from the reader, 124 | /// handle each one with the RpcDispatcher, and write the JSON responses to the buffered_writer. 125 | pub fn runByContentLength(alloc: Allocator, reader: *std.Io.Reader, writer: *std.Io.Writer, 126 | rpc_dispatcher: *RpcDispatcher, options: ContentLengthOptions) !void { 127 | const dispatcher = RequestDispatcher.implBy(rpc_dispatcher); 128 | try requestsByContentLength(alloc, reader, writer, dispatcher, options); 129 | } 130 | 131 | /// Runs a loop to read a stream of Content-length based JSON request messages (frames) from the reader, 132 | /// handle each one with the dispatcher interface, and write the JSON responses to the buffered_writer. 133 | pub fn requestsByContentLength(alloc: Allocator, reader: *std.Io.Reader, writer: *std.Io.Writer, 134 | dispatcher: RequestDispatcher, options: ContentLengthOptions) !void { 135 | options.logger.start("[stream.requestsByContentLength] Logging starts"); 136 | defer { options.logger.stop("[stream.requestsByContentLength] Logging stops"); } 137 | 138 | var frame_data = frame.FrameData.init(alloc); 139 | defer frame_data.deinit(); 140 | // var response_buf = std.Io.Writer.Allocating.init(alloc); 141 | // defer response_buf.deinit(); 142 | var pipeline = try zigjr.RequestPipeline.init(alloc, dispatcher, options.logger); 143 | defer pipeline.deinit(); 144 | 145 | while (true) { 146 | frame_data.reset(); 147 | const has_data = frame.readContentLengthFrame(reader, &frame_data) catch |err| { 148 | if (err == JrErrors.MissingContentLengthHeader and options.recover_on_missing_header) { 149 | continue; 150 | } 151 | return err; // unrecoverable error while reading from reader. 152 | }; 153 | if (!has_data) 154 | break; 155 | 156 | const request_json = std.mem.trim(u8, frame_data.getContent(), " \t"); 157 | if (options.skip_blank_message and request_json.len == 0) continue; 158 | 159 | // response_buf.clearRetainingCapacity(); // reset the output buffer for every request. 160 | options.logger.log("stream.requestsByContentLength", "request ", request_json); 161 | 162 | const run_status = try pipeline.runRequest(request_json); 163 | if (run_status.hasReply()) { 164 | try frame.writeContentLengthFrame(writer, pipeline.responseJson()); 165 | try writer.flush(); 166 | options.logger.log("stream.requestsByContentLength", "response", pipeline.responseJson()); 167 | // try frame.writeContentLengthFrame(writer, response_buf.written()); 168 | // try writer.flush(); 169 | // options.logger.log("stream.requestsByContentLength", "response", response_buf.written()); 170 | } else { 171 | options.logger.log("stream.requestsByContentLength", "response", ""); 172 | } 173 | if (run_status.end_stream) { 174 | break; 175 | } 176 | } 177 | } 178 | 179 | /// Runs a loop to read a stream of JSON response messages (frames) from the reader, 180 | /// and handle each one with the dispatcher. 181 | pub fn responsesByContentLength(alloc: Allocator, reader: anytype, 182 | dispatcher: ResponseDispatcher, options: ContentLengthOptions) !void { 183 | var frame_buf = frame.FrameData.init(alloc); 184 | defer frame_buf.deinit(); 185 | var pipeline = try zigjr.ResponsePipeline.init(alloc, dispatcher); 186 | defer pipeline.deinit(); 187 | 188 | options.logger.start("[stream.responsesByContentLength] Logging starts"); 189 | defer { options.logger.stop("[stream.responsesByContentLength] Logging stops"); } 190 | 191 | while (true) { 192 | frame_buf.reset(); 193 | if (!try frame.readContentLengthFrame(reader, &frame_buf)) 194 | break; 195 | 196 | const response_json = std.mem.trim(u8, frame_buf.getContent(), " \t"); 197 | if (options.skip_blank_message and response_json.len == 0) continue; 198 | 199 | options.logger.log("stream.responsesByContentLength", "receive response", response_json); 200 | pipeline.runResponse(response_json, null) catch |err| { 201 | var stderr_writer = std.fs.File.stderr().writer(&.{}); 202 | const stderr = &stderr_writer.interface; 203 | stderr.print("Error in runResponse(). {any}", .{err}) catch {}; 204 | }; 205 | } 206 | } 207 | 208 | pub const ContentLengthOptions = struct { 209 | recover_on_missing_header: bool = true, 210 | skip_blank_message: bool = true, 211 | logger: zigjr.Logger = zigjr.Logger.implBy(&nopLogger), 212 | }; 213 | 214 | var nopLogger = zigjr.NopLogger{}; 215 | 216 | 217 | /// Runs a loop to read a stream of JSON request and/or response messages (frames) from the reader, 218 | /// and handle each one with the RequestDispatcher or the ResponseDispatcher. 219 | pub fn messagesByContentLength(alloc: Allocator, reader: anytype, req_writer: anytype, 220 | req_dispatcher: RequestDispatcher, res_dispatcher: ResponseDispatcher, 221 | options: ContentLengthOptions) !void { 222 | var frame_buf = frame.FrameData.init(alloc); 223 | defer frame_buf.deinit(); 224 | const req_output_writer = req_writer; 225 | var pipeline = try zigjr.MessagePipeline.init(alloc, req_dispatcher, res_dispatcher, options.logger); 226 | defer pipeline.deinit(); 227 | 228 | options.logger.start("[stream.messagesByContentLength] Logging starts"); 229 | defer { options.logger.stop("[stream.messagesByContentLength] Logging stops"); } 230 | 231 | while (true) { 232 | frame_buf.reset(); 233 | if (!try frame.readContentLengthFrame(reader, &frame_buf)) 234 | break; 235 | 236 | const message_json = std.mem.trim(u8, frame_buf.getContent(), " \t"); 237 | if (options.skip_blank_message and message_json.len == 0) continue; 238 | 239 | const run_status = try pipeline.runMessage(message_json); 240 | switch (run_status.kind) { 241 | .request => { 242 | if (run_status.hasReply()) { 243 | try frame.writeContentLengthFrame(req_output_writer, pipeline.reqResponseJson()); 244 | try req_output_writer.flush(); 245 | options.logger.log("stream.messagesByContentLength", "request_has_response", pipeline.reqResponseJson()); 246 | } else { 247 | options.logger.log("stream.messagesByContentLength", "request_no_response", ""); 248 | } 249 | }, 250 | .response => { 251 | options.logger.log("stream.messagesByContentLength", "response_processed", ""); 252 | }, 253 | } 254 | 255 | if (run_status.end_stream) { 256 | break; 257 | } 258 | } 259 | } 260 | 261 | -------------------------------------------------------------------------------- /examples/lsp_client.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | // Note: For now this example doesn't work on Windows with the Zig 0.15.1 changes. 10 | 11 | const std = @import("std"); 12 | const Allocator = std.mem.Allocator; 13 | const Thread = std.Thread; 14 | const Mutex = std.Thread.Mutex; 15 | const ArrayList = std.ArrayList; 16 | const Value = std.json.Value; 17 | const ObjectMap = std.json.ObjectMap; 18 | const allocPrint = std.fmt.allocPrint; 19 | const StringifyOptions = std.json.Stringify.Options; 20 | 21 | const zigjr = @import("zigjr"); 22 | const RpcId = zigjr.RpcId; 23 | const writeContentLengthRequest = zigjr.frame.writeContentLengthRequest; 24 | const responsesByContentLength = zigjr.stream.responsesByContentLength; 25 | const messagesByContentLength = zigjr.stream.messagesByContentLength; 26 | const RpcDispatcher = zigjr.RpcDispatcher; 27 | const RequestDispatcher = zigjr.RequestDispatcher; 28 | const ResponseDispatcher = zigjr.ResponseDispatcher; 29 | const RpcRequest = zigjr.RpcRequest; 30 | const RpcResponse = zigjr.RpcResponse; 31 | const DispatchResult = zigjr.DispatchResult; 32 | 33 | const MyErrors = error{ MissingCfg, MissingCmd, MissingSourceFile }; 34 | 35 | 36 | /// A LSP client example that spawns a LSP server as a sub-process and 37 | /// talks to it over the stdin/stdout transport. 38 | pub fn main() !void { 39 | var gpa = std.heap.DebugAllocator(.{}){}; 40 | defer _ = gpa.deinit(); 41 | const alloc = gpa.allocator(); 42 | 43 | var args = try CmdArgs.init(alloc); 44 | defer args.deinit(); 45 | args.parse() catch { usage(); return; }; 46 | std.debug.print("[lsp_client] LSP server cmd: {s}\n", .{ args.cmd_argv.items[0] }); 47 | 48 | var child = std.process.Child.init(args.cmd_argv.items, alloc); 49 | child.stdin_behavior = .Pipe; 50 | child.stdout_behavior = .Pipe; 51 | child.stderr_behavior = if (args.stderr) .Inherit else .Ignore; 52 | try child.spawn(); 53 | 54 | const request_thread = try Thread.spawn(.{}, request_worker, .{ child.stdin.? }); 55 | const response_thread = try Thread.spawn(.{}, response_worker, .{ child.stdout.?, args }); 56 | 57 | request_thread.join(); 58 | response_thread.join(); 59 | 60 | child.stdin = null; // already closed by the request_worker; clear so it won't be closed again. 61 | _ = try child.wait(); 62 | } 63 | 64 | fn usage() void { 65 | std.debug.print( 66 | \\Usage: lsp_client [--json | --pp-json | --dump | --stderr ] lsp_server [arguments] 67 | \\ --json print the JSON result from server. 68 | \\ --pp-json pretty-print the JSON result from server. 69 | \\ --dump dump the raw response messages. 70 | \\ --stderr print LSP server's stderr to this process' stderr. 71 | \\ 72 | \\e.g. lsp_client /zls/zls.exe 73 | \\e.g. lsp_client --pp-json /zls/zls.exe 74 | \\e.g. lsp_client --pp-json /zls/zls.exe --enable-stderr-logs 75 | , .{}); 76 | } 77 | 78 | // Poorman's quick and dirty command line argument parsing. 79 | const CmdArgs = struct { 80 | alloc: Allocator, 81 | arg_itr: std.process.ArgIterator, 82 | cmd_argv: ArrayList([]const u8), 83 | json: bool = false, 84 | pp_json: bool = false, 85 | dump: bool = false, 86 | stderr: bool = false, 87 | 88 | fn init(alloc: Allocator) !@This() { 89 | return .{ 90 | .alloc = alloc, 91 | .arg_itr = try std.process.argsWithAllocator(alloc), 92 | .cmd_argv = .empty, 93 | }; 94 | } 95 | 96 | fn deinit(self: *@This()) void { 97 | self.arg_itr.deinit(); 98 | self.cmd_argv.deinit(self.alloc); 99 | } 100 | 101 | fn parse(self: *@This()) !void { 102 | var argv = self.arg_itr; 103 | _ = argv.next(); // skip this program's name. 104 | while (argv.next())|argz| { 105 | const arg = std.mem.sliceTo(argz, 0); 106 | if (false) {} 107 | else if (std.mem.eql(u8, arg, "--json")) { self.json = true; } 108 | else if (std.mem.eql(u8, arg, "--pp-json")) { self.pp_json = true; } 109 | else if (std.mem.eql(u8, arg, "--dump")) { self.dump = true; } 110 | else if (std.mem.eql(u8, arg, "--stderr")) { self.stderr = true; } 111 | else { try self.cmd_argv.append(self.alloc, arg); } // collect the lsp-server cmd and args. 112 | } 113 | 114 | if (self.cmd_argv.items.len == 0) return error.MissingCmd; 115 | } 116 | 117 | }; 118 | 119 | fn request_worker(in_stdin: std.fs.File) !void { 120 | var gpa = std.heap.DebugAllocator(.{}){}; 121 | const alloc = gpa.allocator(); 122 | var writer_buf: [1024]u8 = undefined; 123 | var in_fwriter = in_stdin.writer(&writer_buf); 124 | const in_writer = &in_fwriter.interface; 125 | var id: i64 = 1; 126 | 127 | std.debug.print("\n[==== request_worker ====] starts\n", .{}); 128 | 129 | std.Thread.sleep(1_000_000_000); // Wait a bit to let the LSP server to come up. 130 | std.debug.print("\n[==== request_worker ====] Send 'initialize' message. id: {}\n", .{id}); 131 | const initializeParams = InitializeParams { 132 | .rootUri = "file:///tmp", 133 | .capabilities = .{}, 134 | }; 135 | try writeContentLengthRequest(alloc, in_writer, "initialize", initializeParams, RpcId.of(id)); 136 | id += 1; 137 | 138 | std.Thread.sleep(1_000_000_000); 139 | std.debug.print("\n[==== request_worker ====] Send 'initialized' notification. id: none\n", .{}); 140 | try writeContentLengthRequest(alloc, in_writer, "initialized", InitializedParams{}, RpcId.ofNone()); 141 | id += 1; 142 | 143 | std.Thread.sleep(1_000_000_000); 144 | std.debug.print("\n[==== request_worker ====] Send 'textDocument/didOpen' notification. id: none\n", .{}); 145 | const didOpenTextDocumentParams = DidOpenTextDocumentParams { 146 | .textDocument = TextDocumentItem { 147 | .uri = "file:///tmp/foo.zig", 148 | .languageId = "zig", 149 | .version = 1, 150 | .text = 151 | \\ fn add(a: i64, b: i64) i64 { 152 | \\ return a + b; 153 | \\ } 154 | \\ fn inc1(a: i64) i64 { 155 | \\ return 1 + add 156 | \\ } 157 | , 158 | }, 159 | }; 160 | try writeContentLengthRequest(alloc, in_writer, "textDocument/didOpen", didOpenTextDocumentParams, RpcId.ofNone()); 161 | id += 1; 162 | 163 | std.Thread.sleep(1_000_000_000); 164 | std.debug.print("\n[==== request_worker ====] Send 'textDocument/definition' message. id: {}\n", .{id}); 165 | const definitionParams = DefinitionParams { 166 | .textDocument = .{ .uri = "file:///tmp/foo.zig" }, 167 | .position = .{ .line = 1, .character = 17 }, // at the "b" parameter 168 | }; 169 | try writeContentLengthRequest(alloc, in_writer, "textDocument/definition", definitionParams, RpcId.of(id)); 170 | id += 1; 171 | 172 | std.Thread.sleep(1_000_000_000); 173 | std.debug.print("\n[==== request_worker ====] Send 'textDocument/hover' message. id: {}\n", .{id}); 174 | const hoverParams = HoverParams { 175 | .textDocument = .{ .uri = "file:///tmp/foo.zig" }, 176 | .position = .{ .line = 3, .character = 9 }, // at the "inc1" identifier 177 | }; 178 | try writeContentLengthRequest(alloc, in_writer, "textDocument/hover", hoverParams, RpcId.of(id)); 179 | id += 1; 180 | 181 | std.Thread.sleep(1_000_000_000); 182 | 183 | std.debug.print("\n[==== request_worker ====] Send 'textDocument/signatureHelp' message. id: {}\n", .{id}); 184 | const signatureHelpParams = SignatureHelpParams { 185 | .textDocument = .{ .uri = "file:///tmp/foo.zig" }, 186 | .position = .{ .line = 3, .character = 9 }, // at the "inc1" identifier 187 | }; 188 | try writeContentLengthRequest(alloc, in_writer, "textDocument/signatureHelp", signatureHelpParams, RpcId.of(id)); 189 | id += 1; 190 | 191 | std.Thread.sleep(1_000_000_000); 192 | std.debug.print("\n[==== request_worker ====] Send 'textDocument/completion' message. id: {}\n", .{id}); 193 | const completionParams = CompletionParams { 194 | .textDocument = .{ .uri = "file:///tmp/foo.zig" }, 195 | .position = .{ .line = 2, .character = 19 }, // right after the "add" identifier 196 | }; 197 | try writeContentLengthRequest(alloc, in_writer, "textDocument/completion", completionParams, RpcId.of(id)); 198 | id += 1; 199 | 200 | std.Thread.sleep(1_000_000_000); 201 | std.debug.print("\n[==== request_worker ====] Send 'shutdown' request. id: {}\n", .{id}); 202 | try writeContentLengthRequest(alloc, in_writer, "shutdown", null, RpcId.of(id)); 203 | id += 1; 204 | 205 | std.Thread.sleep(1_000_000_000); 206 | std.debug.print("\n[==== request_worker ====] Send 'exit' notification\n", .{}); 207 | try writeContentLengthRequest(alloc, in_writer, "exit", null, RpcId.ofNone()); 208 | id += 1; 209 | 210 | std.Thread.sleep(1_000_000_000); 211 | in_stdin.close(); // send an EOF signal to subprocess in case shutdown/exit didn't work. 212 | std.debug.print("\n[==== request_worker ====] exits\n", .{}); 213 | } 214 | 215 | fn response_worker(child_stdout: std.fs.File, args: CmdArgs) !void { 216 | var gpa = std.heap.DebugAllocator(.{}){}; 217 | const alloc = gpa.allocator(); 218 | 219 | var reader_buf: [1024]u8 = undefined; 220 | var out_freader = child_stdout.readerStreaming(&reader_buf); 221 | var out_reader = &out_freader.interface; 222 | std.debug.print("[---- response_worker ---] starts\n", .{}); 223 | 224 | if (args.dump) { 225 | // Dump the raw messages from LSP server. 226 | var buf: ArrayList(u8) = .empty; 227 | defer buf.deinit(alloc); 228 | var chunk: [1024]u8 = undefined; 229 | while (true) { 230 | const chunk_len = try out_reader.readSliceShort(&chunk); 231 | if (chunk_len == 0) break; 232 | try buf.appendSlice(alloc, chunk[0..chunk_len]); 233 | if (chunk_len == 1024) 234 | continue; // if the msg aligns at 1024, will read the next msg and combine both. 235 | std.debug.print("\n[---- response_worker ---] Server json:\n{s}\n\n", .{buf.items}); 236 | } 237 | } else { 238 | // LSP server can send 'server_to_client' notifications/events as JSON-RPC requests. 239 | // Use ZigJR's RpcRegistry and Fallback to process the request messages. 240 | var rpc_dispatcher = try zigjr.RpcDispatcher.init(alloc); 241 | defer rpc_dispatcher.deinit(); 242 | 243 | try rpc_dispatcher.add(zigjr.H_PRE_REQUEST, onBefore); 244 | 245 | var fallbackCtx: FallbackCtx = .{ 246 | .log_json = (args.json or args.pp_json), 247 | .json_opt = if (args.pp_json) .{ .whitespace = .indent_2 } else .{}, 248 | }; 249 | try rpc_dispatcher.addWithCtx(zigjr.H_FALLBACK, &fallbackCtx, onFallback); 250 | 251 | // Comment out this to see the fallback_handler being called. 252 | try rpc_dispatcher.add("window/logMessage", window_logMessage); 253 | 254 | // Use ZigJR's ResponseDispatcher to process the response messages. 255 | var res_dispatcher = ResDispatcher { 256 | .log_json = (args.json or args.pp_json), 257 | .json_opt = if (args.pp_json) .{ .whitespace = .indent_2 } else .{}, 258 | }; 259 | 260 | var stderr_buffer: [1024]u8 = undefined; 261 | var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer); 262 | const stderr = &stderr_writer.interface; 263 | 264 | // Use the generic 'messagesByContentLength' to handle both requests and responses. 265 | try messagesByContentLength(alloc, out_reader, stderr, 266 | RequestDispatcher.implBy(&rpc_dispatcher), 267 | ResponseDispatcher.implBy(&res_dispatcher), .{}); 268 | } 269 | 270 | std.debug.print("[---- response_worker ---] exits\n", .{}); 271 | } 272 | 273 | fn onBefore(dc: *zigjr.DispatchCtx) void { 274 | // req.result has the result JSON object from server. 275 | // req.id is the request id; dispatch based on the id recorded in request_worker(). 276 | std.debug.print("\n[---- response_worker ---] Server sent request, method: {s}, id: {any}\n", 277 | .{dc.request().method, dc.request().id}); 278 | } 279 | 280 | const FallbackCtx = struct { 281 | log_json: bool, 282 | json_opt: StringifyOptions, 283 | }; 284 | 285 | 286 | fn onFallback(ctx: *FallbackCtx, dc: *zigjr.DispatchCtx) anyerror!DispatchResult { 287 | // const ctx = @as(*FallbackCtx, @ptrCast(@alignCast(ctx_ptr))); 288 | if (ctx.log_json) { 289 | const params_json = try std.json.Stringify.valueAlloc(dc.arena(), dc.request().params, ctx.json_opt); 290 | std.debug.print("{s}\n", .{params_json}); 291 | } 292 | return DispatchResult.asNone(); 293 | } 294 | 295 | // Handler for the 'window/logMessage' request from server. 296 | fn window_logMessage(params: LogMessageParams) void { 297 | std.debug.print("type: {}\nmessage: {s}\n", .{params.@"type", params.message}); 298 | } 299 | 300 | const ResDispatcher = struct { 301 | log_json: bool, 302 | json_opt: StringifyOptions, 303 | 304 | pub fn dispatch(self: *@This(), alloc: Allocator, res: RpcResponse) anyerror!bool { 305 | // res.result has the result JSON object from server. 306 | // res.id is the request id; dispatch based on the id recorded in request_worker(). 307 | if (res.hasErr()) { 308 | std.debug.print("\n[---- response_worker ---] Server sent error response, error code: {}, msg, {s}\n", 309 | .{res.err().code, res.err().message}); 310 | } else { 311 | std.debug.print("\n[---- response_worker ---] Server sent response, id: {any}\n", .{res.id.num}); 312 | if (self.log_json) { 313 | const result_json = try std.json.Stringify.valueAlloc(alloc, res.result, self.json_opt); 314 | defer alloc.free(result_json); 315 | std.debug.print("{s}\n", .{result_json}); 316 | } 317 | } 318 | return true; 319 | } 320 | }; 321 | 322 | 323 | // LSP messages, with much omissions. 324 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/ 325 | 326 | const InitializeParams = struct { 327 | processId: ?i32 = null, 328 | clientInfo: ?struct { 329 | name: []const u8, // The name of the client. 330 | version: ?[]const u8 = null, // The client's version. 331 | } = null, 332 | locale: ?[]const u8 = null, 333 | rootUri: ?[]const u8 = null, // rootPath of the workspace 334 | capabilities: ClientCapabilities, // client capabilities 335 | }; 336 | 337 | const ClientCapabilities = struct { 338 | }; 339 | 340 | const InitializedParams = struct { 341 | }; 342 | 343 | const DidOpenTextDocumentParams = struct { 344 | textDocument: TextDocumentItem, 345 | }; 346 | 347 | const TextDocumentItem = struct { 348 | uri: []const u8, 349 | languageId: []const u8, 350 | version: i32, 351 | text: []const u8, 352 | }; 353 | 354 | const CompletionParams = struct { 355 | textDocument: TextDocumentIdentifier, 356 | position: Position, 357 | }; 358 | 359 | const HoverParams = struct { 360 | textDocument: TextDocumentIdentifier, 361 | position: Position, 362 | }; 363 | 364 | const SignatureHelpParams = struct { 365 | textDocument: TextDocumentIdentifier, 366 | position: Position, 367 | }; 368 | 369 | const DefinitionParams = struct { 370 | textDocument: TextDocumentIdentifier, 371 | position: Position, 372 | }; 373 | 374 | const TextDocumentIdentifier = struct { 375 | uri: []const u8, 376 | }; 377 | 378 | const Position = struct { 379 | line: u32, 380 | character: u32, 381 | }; 382 | 383 | const LogMessageParams = struct { 384 | @"type": MessageType, 385 | message: []const u8, 386 | }; 387 | 388 | const MessageType = enum(i64) { 389 | Error = 1, 390 | Warning = 2, 391 | Info = 3, 392 | Log = 4, 393 | Debug = 5, 394 | }; 395 | 396 | -------------------------------------------------------------------------------- /examples/mcp_hello.zig: -------------------------------------------------------------------------------- 1 | // Zig JR 2 | // A Zig based JSON-RPC 2.0 library. 3 | // Copyright (C) 2025 William W. Wong. All rights reserved. 4 | // (williamw520@gmail.com) 5 | // 6 | // MIT License. See the LICENSE file. 7 | // 8 | 9 | const std = @import("std"); 10 | const Allocator = std.mem.Allocator; 11 | const Value = std.json.Value; 12 | const ObjectMap = std.json.ObjectMap; 13 | const allocPrint = std.fmt.allocPrint; 14 | 15 | const zigjr = @import("zigjr"); 16 | const Logger = zigjr.Logger; 17 | 18 | 19 | /// A simple MCP server example using the stdin/stdout transport. It implements: 20 | /// - MCP handshake 21 | /// - MCP tool discovery 22 | /// - MCP tool call 23 | /// - A few sample tools: 24 | /// hello: replies "Hello World!" when called. 25 | /// hello-name: replies "Hello 'NAME'!" when called with a name. 26 | /// hello-xtimes: replies "Hello 'NAME'" X times when called with a name and a number. 27 | pub fn main() !void { 28 | var gpa = std.heap.DebugAllocator(.{}){}; 29 | defer _ = gpa.deinit(); 30 | const alloc = gpa.allocator(); 31 | 32 | // Getting the stdin and stdout boilerplate. 33 | var stdin_buffer: [1024]u8 = undefined; 34 | var stdin_reader = std.fs.File.stdin().reader(&stdin_buffer); 35 | const stdin = &stdin_reader.interface; 36 | 37 | var stdout_buffer: [1024]u8 = undefined; 38 | var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); 39 | const stdout = &stdout_writer.interface; 40 | 41 | // Use a file-based logger since the executable is run in a MCP host 42 | // as a sub-process and cannot log to the host's stdout. 43 | var f_logger = try zigjr.FileLogger.init(alloc, "log.txt"); 44 | defer f_logger.deinit(); 45 | 46 | var rpc_dispatcher = try zigjr.RpcDispatcher.init(alloc); 47 | defer rpc_dispatcher.deinit(); 48 | 49 | // Register the MCP RPC methods. 50 | try rpc_dispatcher.add("initialize", mcp_initialize); 51 | try rpc_dispatcher.add("notifications/initialized", mcp_notifications_initialized); 52 | try rpc_dispatcher.add("tools/list", mcp_tools_list); 53 | try rpc_dispatcher.add("tools/call", mcp_tools_call); 54 | 55 | // Starts the JSON streaming pipeline from stdin to stdout. 56 | const dispatcher = zigjr.RequestDispatcher.implBy(&rpc_dispatcher); 57 | try zigjr.stream.requestsByDelimiter(alloc, stdin, stdout, dispatcher, 58 | .{ .logger = f_logger.asLogger() }); 59 | } 60 | 61 | // The MCP message handlers. 62 | // 63 | // All the MCP message parameters and return values are defined in below 64 | // as Zig structs or tagged unions according to the MCP JSON schema spec. 65 | // ZigJR automatically does the mapping between the structs and the JSON objects. 66 | 67 | // First message from a MCP client to do the initial handshake. 68 | fn mcp_initialize(dc: *zigjr.DispatchCtx, params: InitializeRequest_params) !InitializeResult { 69 | const msg = try allocPrint(dc.arena(), "client name: {s}, version: {s}", .{params.clientInfo.name, params.clientInfo.version}); 70 | dc.logger().log("mcp_initialize", "initialize", msg); 71 | 72 | return .{ 73 | .protocolVersion = "2025-03-26", // https://github.com/modelcontextprotocol/modelcontextprotocol/tree/main/schema/2025-03-26 74 | .serverInfo = Implementation { 75 | .name = "zigjr mcp_hello", 76 | .version = "1.0.0", 77 | }, 78 | .instructions = "It has a number of tools for replying to the 'hello' requests.", 79 | .capabilities = ServerCapabilities { 80 | .tools = .{}, // server capable of doing tools 81 | }, 82 | }; 83 | } 84 | 85 | fn mcp_notifications_initialized(dc: *zigjr.DispatchCtx, params: InitializedNotification_params) !void { 86 | const msg = try allocPrint(dc.arena(), "params: {any}", .{params}); 87 | dc.logger().log("mcp_notifications_initialized", "notifications/initialized", msg); 88 | } 89 | 90 | fn mcp_tools_list(dc: *zigjr.DispatchCtx, params: ListToolsRequest_params) !ListToolsResult { 91 | const msg = try allocPrint(dc.arena(), "params: {any}", .{params}); 92 | dc.logger().log("mcp_tools_list", "tools/list", msg); 93 | var tools: std.ArrayList(Tool) = .empty; 94 | 95 | const helloTool = Tool.init(dc.arena(), "hello", "Replying a 'Hello World' when called."); 96 | try tools.append(dc.arena(), helloTool); 97 | 98 | var helloNameTool = Tool.init(dc.arena(), "hello-name", "Replying a 'Hello NAME' when called with a NAME."); 99 | try helloNameTool.inputSchema.addProperty("name", "string", "The name to say hello to"); 100 | try helloNameTool.inputSchema.addRequired("name"); 101 | try tools.append(dc.arena(), helloNameTool); 102 | 103 | var helloXTimesTool = Tool.init(dc.arena(), "hello-xtimes", "Replying a 'Hello NAME NUMBER' repeated X times when called with a NAME and a number."); 104 | try helloXTimesTool.inputSchema.addProperty("name", "string", "The name to say hello to"); 105 | try helloXTimesTool.inputSchema.addRequired("name"); 106 | try helloXTimesTool.inputSchema.addProperty("times", "integer", "The number of times to repeat"); 107 | try helloXTimesTool.inputSchema.addRequired("times"); 108 | try tools.append(dc.arena(), helloXTimesTool); 109 | 110 | return .{ 111 | .tools = tools, 112 | }; 113 | } 114 | 115 | fn mcp_tools_call(dc: *zigjr.DispatchCtx, params: Value) !CallToolResult { 116 | const tool = params.object.get("name").?.string; 117 | const msg = try allocPrint(dc.arena(), "tool name: {s}", .{tool}); 118 | dc.logger().log("mcp_tools_call", "tools/call", msg); 119 | 120 | // We'll just do a poorman's dispatching on the MCP tool name. 121 | if (std.mem.eql(u8, tool, "hello")) { 122 | var ctr = CallToolResult.init(dc.arena()); 123 | try ctr.addTextContent("Hello World!"); 124 | return ctr; 125 | } else if (std.mem.eql(u8, tool, "hello-name")) { 126 | const arguments = params.object.get("arguments").?.object; 127 | const name = arguments.get("name") orelse Value{ .string = "not set" }; 128 | dc.logger().log("mcp_tools_call", "arguments", name.string); 129 | 130 | var ctr = CallToolResult.init(dc.arena()); 131 | try ctr.addTextContent(try allocPrint(dc.arena(), "Hello '{s}'!", .{name.string})); 132 | return ctr; 133 | } else if (std.mem.eql(u8, tool, "hello-xtimes")) { 134 | const arguments = params.object.get("arguments").?.object; 135 | const name = arguments.get("name") orelse Value{ .string = "not set" }; 136 | const times = arguments.get("times") orelse Value{ .integer = 1 }; 137 | const repeat: usize = if (0 < times.integer and times.integer < 100) @intCast(times.integer) else 1; 138 | var buf = std.Io.Writer.Allocating.init(dc.arena()); 139 | var writer = &buf.writer; 140 | for (0..repeat) |_| try writer.print("Hello {s}! ", .{name.string}); 141 | var ctr = CallToolResult.init(dc.arena()); 142 | try ctr.addTextContent(buf.written()); 143 | return ctr; 144 | } else { 145 | var ctr = CallToolResult.init(dc.arena()); 146 | try ctr.addTextContent(try allocPrint(dc.arena(), "Tool {s} not found", .{tool})); 147 | ctr.isError = true; 148 | return ctr; 149 | } 150 | } 151 | 152 | 153 | // The following MCP message structs should have been put in a library, 154 | // but they're put here to illustrate that it's not too hard to come up with a MCP server. 155 | 156 | /// See the MCP message schema definition and sample messages for detail. 157 | /// https://github.com/modelcontextprotocol/modelcontextprotocol/tree/main/schema/2025-03-26 158 | /// https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/lifecycle.mdx 159 | /// https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/server/tools.mdx 160 | 161 | const InitializeRequest_params = struct { 162 | protocolVersion: []const u8, 163 | capabilities: ClientCapabilities, 164 | clientInfo: Implementation, 165 | }; 166 | 167 | const Implementation = struct { 168 | name: []const u8, 169 | version: []const u8, 170 | }; 171 | 172 | const ClientCapabilities = struct { 173 | roots: ?struct { 174 | listChanged: bool, 175 | } = null, 176 | sampling: ?Value = null, 177 | experimental: ?Value = null, 178 | }; 179 | 180 | const InitializeResult = struct { 181 | _meta: ?Value = null, 182 | protocolVersion: []const u8, 183 | capabilities: ServerCapabilities, 184 | serverInfo: Implementation, 185 | instructions: ?[]const u8, 186 | }; 187 | 188 | const InitializedNotification_params = struct { 189 | _meta: ?Value = null, 190 | }; 191 | 192 | const ServerCapabilities = struct { 193 | completions: ?Value = null, 194 | experimental: ?Value = null, 195 | logging: ?Value = null, 196 | prompts: ?struct { 197 | listChanged: ?bool = false, 198 | } = null, 199 | resources: ?struct { 200 | listChanged: ?bool = false, 201 | subscribe: ?bool = false, 202 | } = null, 203 | tools: ?struct { 204 | listChanged: ?bool = false, 205 | } = null, 206 | }; 207 | 208 | const ListToolsRequest_params = struct { 209 | cursor: ?[]const u8 = null, 210 | }; 211 | 212 | const ListToolsResult = struct { 213 | _meta: ?Value = null, 214 | nextCursor: ?[]const u8 = null, 215 | tools: std.ArrayList(Tool), 216 | 217 | pub fn jsonStringify(self: @This(), jsonWriteStream: anytype) !void { 218 | const jws = jsonWriteStream; 219 | try jws.beginObject(); 220 | { 221 | if (self._meta) |val| try writeField(jws, "_meta", val); 222 | if (self.nextCursor) |val| try writeField(jws, "nextCursor", val); 223 | try jws.objectField("tools"); 224 | try jws.write(self.tools.items); 225 | } 226 | try jws.endObject(); 227 | } 228 | }; 229 | 230 | const Tool = struct { 231 | name: []const u8, 232 | description: ?[]const u8 = null, 233 | inputSchema: InputSchema, 234 | annotations: ?ToolAnnotations = null, 235 | 236 | pub fn init(alloc: Allocator, name: []const u8, desc: []const u8) @This() { 237 | return .{ 238 | .name = name, 239 | .description = desc, 240 | .inputSchema = InputSchema.init(alloc), 241 | }; 242 | } 243 | 244 | pub fn deinit(self: *@This()) void { 245 | self.inputSchema.deinit(); 246 | } 247 | 248 | }; 249 | 250 | const InputSchema = struct { 251 | alloc: Allocator, 252 | @"type": [6]u8 = "object".*, 253 | properties: Value, 254 | required: std.ArrayList([]const u8), 255 | 256 | pub fn init(alloc: Allocator) @This() { 257 | return .{ 258 | .alloc = alloc, 259 | .properties = jsonObject(alloc), 260 | .required = .empty, 261 | }; 262 | } 263 | 264 | pub fn deinit(self: *@This()) void { 265 | self.properties.object.deinit(); 266 | self.required.deinit(); 267 | } 268 | 269 | pub fn jsonStringify(self: @This(), jsonWriteStream: anytype) !void { 270 | const jws = jsonWriteStream; 271 | try jws.beginObject(); 272 | { 273 | try jws.objectField("type"); 274 | try jws.write(self.@"type"); 275 | 276 | try jws.objectField("properties"); 277 | try jws.write(self.properties); 278 | 279 | try jws.objectField("required"); 280 | try jws.write(self.required.items); 281 | } 282 | try jws.endObject(); 283 | } 284 | 285 | pub fn addProperty(self: *@This(), prop_name: []const u8, type_name: []const u8, type_desc: []const u8) !void { 286 | const typeInfo = try typeObject(self.alloc, type_name, type_desc); 287 | try self.properties.object.put(prop_name, typeInfo); 288 | } 289 | 290 | pub fn addRequired(self: *@This(), field_name: []const u8) !void { 291 | try self.required.append(self.alloc, field_name); 292 | } 293 | 294 | }; 295 | 296 | const Annotations = struct { 297 | audience: std.ArrayList(Role), 298 | priority: i64, // "maximum": 1, "minimum": 0 299 | 300 | pub fn jsonStringify(self: @This(), jsonWriteStream: anytype) !void { 301 | const jws = jsonWriteStream; 302 | try jws.beginObject(); 303 | { 304 | try jws.objectField("audience"); 305 | try jws.beginArray(); 306 | for (self.audience.items)|role| { 307 | try jws.write(@tagName(role)); 308 | } 309 | try jws.endArray(); 310 | 311 | try jws.objectField("priority"); 312 | try jws.write(self.priority); 313 | } 314 | try jws.endObject(); 315 | } 316 | }; 317 | 318 | const Role = enum { 319 | assistant, 320 | user 321 | }; 322 | 323 | const ToolAnnotations = struct { 324 | destructiveHint: bool = false, 325 | idempotentHint: bool = false, 326 | openWorldHint: bool = false, 327 | readOnlyHint: bool = false, 328 | title: []const u8, 329 | }; 330 | 331 | const CallToolResult = struct { 332 | alloc: Allocator, 333 | _meta: ?Value = null, 334 | content: std.ArrayList(Content), 335 | isError: bool = false, 336 | 337 | pub fn init(alloc: Allocator) @This() { 338 | return .{ 339 | .alloc = alloc, 340 | .content = .empty 341 | }; 342 | } 343 | 344 | pub fn addTextContent(self: *@This(), text: []const u8) !void { 345 | try self.content.append(self.alloc, Content{ .text = TextContent{ .text = text } }); 346 | } 347 | 348 | pub fn addImageContent(self: *@This(), data: []const u8, mineType: []const u8) !void { 349 | try self.content.append(self.alloc, Content { 350 | .image = ImageContent { 351 | .data = data, 352 | .mimeType = mineType, 353 | } 354 | }); 355 | } 356 | 357 | pub fn addAudioContent(self: *@This(), data: []const u8, mineType: []const u8) !void { 358 | try self.content.append(self.alloc, Content { 359 | .audio = AudioContent { 360 | .data = data, 361 | .mimeType = mineType, 362 | } 363 | }); 364 | } 365 | 366 | pub fn jsonStringify(self: @This(), jsonWriteStream: anytype) !void { 367 | const jws = jsonWriteStream; 368 | try jws.beginObject(); 369 | { 370 | if (self._meta) |val| try writeField(jws, "_meta", val); 371 | 372 | try jws.objectField("content"); 373 | try jws.write(self.content.items); 374 | 375 | try jws.objectField("isError"); 376 | try jws.write(self.isError); 377 | } 378 | try jws.endObject(); 379 | } 380 | 381 | }; 382 | 383 | const Content = union(enum) { 384 | text: TextContent, 385 | image: ImageContent, 386 | audio: AudioContent, 387 | embedded: EmbeddedResource, 388 | 389 | pub fn jsonStringify(self: @This(), jsonWriteStream: anytype) !void { 390 | const jws = jsonWriteStream; 391 | switch (self) { 392 | .text => |text| try jws.write(text), 393 | .image => |image| try jws.write(image), 394 | .audio => |audio| try jws.write(audio), 395 | .embedded => |emb| try jws.write(emb), 396 | } 397 | } 398 | }; 399 | 400 | const TextContent = struct { 401 | @"type": [4]u8 = "text".*, 402 | text: []const u8, 403 | annotations: ?Annotations = null, 404 | }; 405 | 406 | const ImageContent = struct { 407 | @"type": [5]u8 = "image".*, 408 | mimeType: []const u8, 409 | data: []const u8, // the base64-encoded image data. 410 | annotations: ?Annotations = null, 411 | }; 412 | 413 | const AudioContent = struct { 414 | @"type": [5]u8 = "audio".*, 415 | mimeType: []const u8, 416 | data: []const u8, // the base64-encoded audio data. 417 | annotations: ?Annotations = null, 418 | }; 419 | 420 | const EmbeddedResource = struct { 421 | @"type": [8]u8 = "resource".*, 422 | resource: ResourceContents, 423 | annotations: ?Annotations = null, 424 | }; 425 | 426 | const ResourceContents = union(enum) { 427 | text: TextResourceContents, 428 | blob: BlobResourceContents, 429 | 430 | pub fn jsonStringify(self: @This(), jsonWriteStream: anytype) !void { 431 | const jws = jsonWriteStream; 432 | switch (self) { 433 | .text => |text| try jws.write(text), 434 | .blob => |blob| try jws.write(blob), 435 | } 436 | } 437 | }; 438 | 439 | const TextResourceContents = struct { 440 | mimeType: ?[]const u8 = null, 441 | text: []const u8, 442 | uri: []const u8, 443 | }; 444 | 445 | const BlobResourceContents = struct { 446 | mimeType: ?[]const u8 = null, 447 | blob: []const u8, 448 | uri: []const u8, 449 | }; 450 | 451 | 452 | fn jsonObject(alloc: Allocator) Value { 453 | return Value { .object = std.json.ObjectMap.init(alloc) }; 454 | } 455 | 456 | fn typeObject(alloc: Allocator, type_name: []const u8, type_desc: []const u8) !Value { 457 | var value = jsonObject(alloc); 458 | try value.object.put("type", Value { .string = type_name }); 459 | try value.object.put("description", Value { .string = type_desc }); 460 | return value; 461 | } 462 | 463 | fn writeField(jws: anytype, name: []const u8, value: anytype) !void { 464 | try jws.objectField(name); 465 | try jws.write(value); 466 | } 467 | 468 | --------------------------------------------------------------------------------