Skip to content

Commit 7e2b6a0

Browse files
committed
Initial version
0 parents  commit 7e2b6a0

File tree

7 files changed

+222
-0
lines changed

7 files changed

+222
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.vscode

LICENSE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Copyright 2020 David Anthoff
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Project.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name = "JSONRPC"
2+
uuid = "b9b8584e-8fd3-41f9-ad0c-7255d428e418"
3+
authors = ["David Anthoff <[email protected]>"]
4+
version = "0.1.0"
5+
6+
[deps]
7+
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
8+
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
9+
10+
[compat]
11+
JSON = "0.20, 0.21"
12+
13+
[extras]
14+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
15+
16+
[targets]
17+
test = ["Test"]

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# JSONRPC
2+

src/JSONRPC.jl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module JSONRPC
2+
3+
import JSON, UUIDs
4+
5+
export JSONRPCEndpoint, send_notification, send_request, send_success_response, send_error_response
6+
7+
include("core.jl")
8+
9+
end

src/core.jl

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
struct JSONRPCError <: Exception
2+
code::Int
3+
msg::AbstractString
4+
data::Any
5+
end
6+
7+
function Base.showerror(io::IO, ex::JSONRPCError)
8+
error_code_as_string = if ex.code==-32700
9+
"ParseError"
10+
elseif ex.code==-32600
11+
"InvalidRequest"
12+
elseif ex.code==-32601
13+
"MethodNotFound"
14+
elseif ex.code==-32602
15+
"InvalidParams"
16+
elseif ex.code==-32603
17+
"InternalError"
18+
elseif ex.code==-32099
19+
"serverErrorStart"
20+
elseif ex.code==-32000
21+
"serverErrorEnd"
22+
elseif ex.code==-32002
23+
"ServerNotInitialized"
24+
elseif ex.code==-32001
25+
"UnknownErrorCode"
26+
elseif ex.code==-32800
27+
"RequestCancelled"
28+
elseif ex.code==-32801
29+
"ContentModified"
30+
else
31+
"Unkonwn"
32+
end
33+
34+
print(io, error_code_as_string)
35+
print(io, ": ")
36+
print(io, ex.msg)
37+
if ex.data!==nothing
38+
print(io, " (")
39+
print(io, ex.data)
40+
print(io, ")")
41+
end
42+
end
43+
44+
mutable struct JSONRPCEndpoint
45+
pipe_in
46+
pipe_out
47+
48+
out_msg_queue::Channel{Any}
49+
in_msg_queue::Channel{Any}
50+
51+
outstanding_requests::Dict{String, Channel{Any}}
52+
53+
err_handler::Union{Nothing,Function}
54+
55+
function JSONRPCEndpoint(pipe_in, pipe_out, err_handler=nothing)
56+
return new(pipe_in, pipe_out, Channel{Any}(Inf), Channel{Any}(Inf), Dict{String, Channel{Any}}(), err_handler)
57+
end
58+
end
59+
60+
function write_transport_layer(stream, response)
61+
response_utf8 = transcode(UInt8, response)
62+
n = length(response_utf8)
63+
write(stream, "Content-Length: $n\r\n\r\n")
64+
write(stream, response_utf8)
65+
end
66+
67+
function read_transport_layer(stream)
68+
header_dict = Dict{String,String}()
69+
line = chomp(readline(stream))
70+
# Check whether the socket was closed
71+
if line == ""
72+
return nothing
73+
end
74+
while length(line) > 0
75+
h_parts = split(line, ":")
76+
header_dict[chomp(h_parts[1])] = chomp(h_parts[2])
77+
line = chomp(readline(stream))
78+
end
79+
message_length = parse(Int, header_dict["Content-Length"])
80+
message_str = String(read(stream, message_length))
81+
return message_str
82+
end
83+
84+
function Base.run(x::JSONRPCEndpoint)
85+
@async try
86+
for msg in x.out_msg_queue
87+
write_transport_layer(x.pipe_out, msg)
88+
end
89+
catch err
90+
bt = catch_backtrace()
91+
if x.err_handler!==nothing
92+
x.err_handler(err, bt)
93+
else
94+
Base.display_error(stderr, err, bt)
95+
end
96+
end
97+
98+
@async try
99+
while true
100+
message = read_transport_layer(x.pipe_in)
101+
102+
if message===nothing
103+
break
104+
end
105+
106+
message_dict = JSON.parse(message)
107+
108+
if haskey(message_dict, "method")
109+
put!(x.in_msg_queue, message_dict)
110+
else
111+
# This must be a response
112+
id_of_request = message_dict["id"]
113+
114+
channel_for_response = x.outstanding_requests[id_of_request]
115+
put!(channel_for_response, message_dict)
116+
end
117+
end
118+
catch err
119+
bt = catch_backtrace()
120+
if x.err_handler!==nothing
121+
x.err_handler(err, bt)
122+
else
123+
Base.display_error(stderr, err, bt)
124+
end
125+
end
126+
end
127+
128+
function send_notification(x::JSONRPCEndpoint, method::AbstractString, params)
129+
message = Dict("jsonrpc"=>"2.0", "method"=>method, "params"=>params)
130+
131+
message_json = JSON.json(message)
132+
133+
put!(x.out_msg_queue, message_json)
134+
end
135+
136+
function send_request(x::JSONRPCEndpoint, method::AbstractString, params)
137+
id = string(UUIDs.uuid4())
138+
message = Dict("jsonrpc"=>"2.0", "method"=>method, "params"=>params, "id"=>id)
139+
140+
response_channel = Channel{Any}(1)
141+
x.outstanding_requests[id] = response_channel
142+
143+
message_json = JSON.json(message)
144+
145+
put!(x.out_msg_queue, message_json)
146+
147+
response = take!(response_channel)
148+
149+
if haskey(response, "result")
150+
return response["result"]
151+
elseif haskey(response, "error")
152+
error_code = response["error"]["code"]
153+
error_msg = response["error"]["message"]
154+
error_data = get(response["error"], "data", nothing)
155+
throw(JSONRPCError(error_code, error_msg, error_data))
156+
else
157+
throw(JSONRPCError(0, "ERROR AT THE TRANSPORT LEVEL", nothing))
158+
end
159+
end
160+
161+
function get_next_message(endpoint::JSONRPCEndpoint)
162+
msg = take!(endpoint.in_msg_queue)
163+
164+
return msg
165+
end
166+
167+
function send_success_response(endpoint, original_request, result)
168+
response = Dict("jsonrpc"=>"2.0", "id"=>original_request["id"], "result"=>result)
169+
170+
response_json = JSON.json(response)
171+
172+
put!(endpoint.out_msg_queue, response_json)
173+
end
174+
175+
function send_error_response(endpoint, original_request, code, message, data)
176+
response = Dict("jsonrpc"=>"2.0", "id"=>original_request["id"], "error"=>Dict("code"=>code, "message"=>message, "data"=>data))
177+
178+
response_json = JSON.json(response)
179+
180+
put!(endpoint.out_msg_queue, response_json)
181+
end

test/runtests.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
using Test
2+
using JSONRPC
3+
4+
@testset "JSONRPC" begin
5+
end

0 commit comments

Comments
 (0)