diff --git a/.github/workflows/juliaci.yml b/.github/workflows/juliaci.yml index 888a57b3..79056ef5 100644 --- a/.github/workflows/juliaci.yml +++ b/.github/workflows/juliaci.yml @@ -17,3 +17,21 @@ jobs: permissions: write-all secrets: codecov_token: ${{ secrets.CODECOV_TOKEN }} + + json-compat: + name: JSON ${{ matrix.json-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + json-version: ["0.21", "1.7"] + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: "1" + - uses: julia-actions/cache@v2 + - name: Install and verify JSON version + run: | + julia --project=. -e 'using Pkg; Pkg.add(PackageSpec(name="JSON", version="${{ matrix.json-version }}")); using JSON; @assert startswith(string(pkgversion(JSON)), "${{ matrix.json-version }}."); Pkg.status()' + - uses: julia-actions/julia-runtest@v1 diff --git a/Project.toml b/Project.toml index 8d4c995b..8f0896a6 100644 --- a/Project.toml +++ b/Project.toml @@ -14,7 +14,7 @@ TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] -JSON = "0.20, 0.21" +JSON = "0.20, 0.21, 1" julia = "1" CancellationTokens = "2.0.2" diff --git a/src/core.jl b/src/core.jl index 86d66999..1cc8eca7 100644 --- a/src/core.jl +++ b/src/core.jl @@ -186,7 +186,7 @@ Used by the MCP (Model Context Protocol) stdio transport. """ struct NewlineDelimitedFraming <: FramingMode end -mutable struct JSONRPCEndpoint{IOIn<:IO,IOOut<:IO,S<:JSON.Serialization,F<:FramingMode} +mutable struct JSONRPCEndpoint{IOIn<:IO,IOOut<:IO,S<:JSONSerialization,F<:FramingMode} pipe_in::IOIn pipe_out::IOOut @@ -225,7 +225,7 @@ mutable struct JSONRPCEndpoint{IOIn<:IO,IOOut<:IO,S<:JSON.Serialization,F<:Frami close_started::Bool end -JSONRPCEndpoint(pipe_in, pipe_out, serialization::JSON.Serialization=JSON.StandardSerialization(); framing::FramingMode=ContentLengthFraming()) = +JSONRPCEndpoint(pipe_in, pipe_out, serialization::JSONSerialization=DefaultJSONSerialization(); framing::FramingMode=ContentLengthFraming()) = JSONRPCEndpoint( pipe_in, pipe_out, @@ -496,7 +496,7 @@ function start(x::JSONRPCEndpoint) end message_dict = try - JSON.parse(message) + _parse_json(message) catch parse_err # Corrupted/truncated message (e.g. remote process crashed mid-write). # Treat as broken pipe and exit the read loop. @@ -616,7 +616,7 @@ function send_notification(x::JSONRPCEndpoint, method::AbstractString, @nospecia message = Dict("jsonrpc" => "2.0", "method" => method, "params" => params) - message_json = sprint(JSON.show_json, x.serialization, message) + message_json = _serialize_json(x.serialization, message) put!(x.out_msg_queue, message_json) @@ -632,7 +632,7 @@ function send_request(x::JSONRPCEndpoint, method::AbstractString, @nospecialize( response_channel = Channel{Any}(1) x.outstanding_requests[id] = response_channel - message_json = sprint(JSON.show_json, x.serialization, message) + message_json = _serialize_json(x.serialization, message) put!(x.out_msg_queue, message_json) @@ -834,7 +834,7 @@ function send_success_response(endpoint, original_request::Request, @nospecializ response = Dict("jsonrpc" => "2.0", "id" => original_request.id, "result" => result) - response_json = sprint(JSON.show_json, endpoint.serialization, response) + response_json = _serialize_json(endpoint.serialization, response) put!(endpoint.out_msg_queue, response_json) end @@ -848,7 +848,7 @@ function send_error_response(endpoint, original_request::Request, @nospecialize( response = Dict("jsonrpc" => "2.0", "id" => original_request.id, "error" => Dict("code" => code, "message" => message, "data" => data)) - response_json = sprint(JSON.show_json, endpoint.serialization, response) + response_json = _serialize_json(endpoint.serialization, response) put!(endpoint.out_msg_queue, response_json) end diff --git a/src/interface_def.jl b/src/interface_def.jl index 0e047f99..7ea7c81a 100644 --- a/src/interface_def.jl +++ b/src/interface_def.jl @@ -1,24 +1,14 @@ abstract type Outbound end -function JSON.Writer.CompositeTypeWrapper(t::Outbound) - fns = collect(fieldnames(typeof(t))) - dels = Int[] - for i = 1:length(fns) - f = fns[i] - if getfield(t, f) isa Missing - push!(dels, i) - end - end - deleteat!(fns, dels) - JSON.Writer.CompositeTypeWrapper(t, Tuple(fns)) -end - function JSON.lower(a::Outbound) - if nfields(a) > 0 - JSON.Writer.CompositeTypeWrapper(a) - else - nothing + nfields(a) == 0 && return nothing + + fields = Dict{String,Any}() + for field in fieldnames(typeof(a)) + value = getfield(a, field) + ismissing(value) || (fields[string(field)] = value) end + return fields end function field_allows_missing(field::Expr) @@ -60,7 +50,7 @@ macro dict_readable(arg) end ) : nothing) - function $tname(dict::Dict) + function $tname(dict::AbstractDict) end end diff --git a/src/jsoncompat.jl b/src/jsoncompat.jl new file mode 100644 index 00000000..8bc3bb0b --- /dev/null +++ b/src/jsoncompat.jl @@ -0,0 +1,17 @@ +@static if isdefined(JSON, :JSONStyle) + const JSONSerialization = JSON.JSONStyle + const DefaultJSONSerialization = JSON.JSONWriteStyle +else + const JSONSerialization = JSON.Serialization + const DefaultJSONSerialization = JSON.StandardSerialization +end + +function _serialize_json(serialization::JSONSerialization, value) + @static if isdefined(JSON, :JSONStyle) + return JSON.json(value; style=serialization) + else + return sprint(JSON.show_json, serialization, value) + end +end + +_parse_json(value) = JSON.parse(value; dicttype=Dict{String,Any}) diff --git a/src/packagedef.jl b/src/packagedef.jl index eab8e3da..8a1a7756 100644 --- a/src/packagedef.jl +++ b/src/packagedef.jl @@ -2,6 +2,7 @@ export JSONRPCEndpoint, TransportError, EndpointStatus, start, send_notification export FramingMode, ContentLengthFraming, NewlineDelimitedFraming include("pipenames.jl") +include("jsoncompat.jl") include("core.jl") include("typed.jl") include("interface_def.jl") @@ -9,7 +10,7 @@ include("interface_def.jl") function _precompile_() ccall(:jl_generating_output, Cint, ()) == 1 || return nothing - E = JSONRPCEndpoint{Base.PipeEndpoint, Base.PipeEndpoint, JSON.Serializations.StandardSerialization, ContentLengthFraming} + E = JSONRPCEndpoint{Base.PipeEndpoint, Base.PipeEndpoint, DefaultJSONSerialization, ContentLengthFraming} precompile(start, (E,)) precompile(send_notification, (E, String, Any)) precompile(send_request, (E, String, Any)) diff --git a/test/test_json_serialization.jl b/test/test_json_serialization.jl index 1764eb5d..dc851d72 100644 --- a/test/test_json_serialization.jl +++ b/test/test_json_serialization.jl @@ -1,16 +1,19 @@ @testitem "Custom JSON serialization" setup=[NamedPipes] begin using JSON - using JSON: StructuralContext, begin_object, show_pair, end_object, show_json, Serializations.StandardSerialization - - struct OurSerialization <: JSON.Serializations.CommonSerialization end struct OurStruct a::String b::String end - function JSON.show_json(io::StructuralContext, s::OurSerialization, f::OurStruct) - show_json(io, StandardSerialization(), "$(f.a):$(f.b)") + @static if isdefined(JSON, :JSONStyle) + struct OurSerialization <: JSON.JSONStyle end + JSON.StructUtils.lower(::OurSerialization, f::OurStruct) = "$(f.a):$(f.b)" + else + struct OurSerialization <: JSON.Serializations.CommonSerialization end + function JSON.show_json(io::JSON.StructuralContext, ::OurSerialization, f::OurStruct) + JSON.show_json(io, JSON.StandardSerialization(), "$(f.a):$(f.b)") + end end x = OurStruct("Hello", "World") diff --git a/test/test_misc.jl b/test/test_misc.jl index d1c85968..2b87b572 100644 --- a/test/test_misc.jl +++ b/test/test_misc.jl @@ -198,7 +198,11 @@ end using JSON buf = IOBuffer() ep = JSONRPC.JSONRPCEndpoint(buf, buf) - @test ep.serialization isa JSON.Serializations.StandardSerialization + @static if isdefined(JSON, :JSONStyle) + @test ep.serialization isa JSON.JSONWriteStyle + else + @test ep.serialization isa JSON.Serializations.StandardSerialization + end @test ep.status == JSONRPC.status_idle @test ep.err === nothing @test ep.read_task === nothing