Skip to content

Commit 2d29a6e

Browse files
authored
ZMQ server: Encode NaN, Inf, -Inf as null in JSON (#1020)
1 parent eb264ad commit 2d29a6e

6 files changed

Lines changed: 63 additions & 42 deletions

File tree

Manifest.toml

Lines changed: 30 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/Project.toml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,14 @@ authors = ["Deltares and contributors"]
77
projects = ["test"]
88

99
[deps]
10-
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
10+
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
1111
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
12-
StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4"
1312
Wflow = "d48b7d99-76e7-47ae-b1d5-ff0c1cf9a818"
1413
ZMQ = "c2297ded-f4af-51ae-bb23-16f91089e4e1"
1514

1615
[compat]
17-
JSON3 = "1.14"
16+
JSON = "1"
1817
Logging = "1"
19-
StructTypes = "1.10"
2018
Wflow = "1"
2119
ZMQ = "1.2"
2220
julia = "1.10"

server/src/WflowServer.jl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
module WflowServer
22
using ZMQ: ZMQ
3-
using JSON3: JSON3
4-
using StructTypes: StructTypes
3+
using JSON: JSON
54
using Wflow: Wflow
65

76
include("bmi_service.jl")

server/src/server.jl

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,18 +49,29 @@ function shutdown(s::ZMQ.Socket, ctx::ZMQ.Context)
4949
return ZMQ.close(ctx)
5050
end
5151

52+
"""
53+
serialize_response(ret)
54+
55+
Serialize the return value `ret` of a Wflow BMI function to a JSON string. JSON has no
56+
representation for the non-finite floating point values `NaN`, `Inf` and `-Inf`, so these
57+
are encoded as `null`.
58+
"""
59+
function serialize_response(ret)
60+
return JSON.json(ret; allownan = true, nan = "null", inf = "null", ninf = "null")
61+
end
62+
5263
"Error response ZMQ server"
5364
function response(err::AbstractString, s::ZMQ.Socket)
5465
@info "Send error response"
5566
resp = Dict{String, String}("status" => "ERROR", "error" => err)
56-
return ZMQ.send(s, JSON3.write(resp))
67+
return ZMQ.send(s, JSON.json(resp))
5768
end
5869

5970
"Status response ZMQ server"
6071
function response(s::ZMQ.Socket)
6172
@info "Send status response"
6273
resp = Dict{String, String}("status" => "OK")
63-
return ZMQ.send(s, JSON3.write(resp))
74+
return ZMQ.send(s, JSON.json(resp))
6475
end
6576

6677
"Validate JSON request against mapped Struct"
@@ -89,7 +100,7 @@ function wflow_bmi(s::ZMQ.Socket, handler::ModelHandler, f)
89100
response(s)
90101
else
91102
@info "Send response including output from Wflow function `$(f.fn)`"
92-
ZMQ.send(s, JSON3.write(ret; allow_inf = true))
103+
ZMQ.send(s, serialize_response(ret))
93104
end
94105
catch e
95106
@error "Wflow function `$(f.fn)` failed" exception = (e, catch_backtrace())
@@ -151,13 +162,13 @@ function start(port::Int)
151162
while true
152163
# Wait for next request from client
153164
req = ZMQ.recv(socket)
154-
json = JSON3.read(req; allow_inf = true)
165+
json = JSON.parse(req; allownan = true)
155166
@info "Received request to run function `$(json.fn)`..."
156167

157168
if haskey(MAP_STRUCTS, json.fn)
158169
v = valid_request(json)
159170
if isnothing(v)
160-
f = StructTypes.constructfrom(MAP_STRUCTS[json.fn], json)
171+
f = JSON.parse(req, MAP_STRUCTS[json.fn]; allownan = true)
161172
wflow_bmi(socket, handler, f)
162173
else
163174
err = (

server/test/Project.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
[deps]
22
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
33
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
4-
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
4+
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
55
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
66
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
7-
StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4"
87
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
98
TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a"
109
Wflow = "d48b7d99-76e7-47ae-b1d5-ff0c1cf9a818"

server/test/client.jl

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
@testitem "Client server Wflow ZMQ Server" begin
2-
import ZMQ, JSON3, StructTypes, Wflow
2+
import ZMQ, JSON, Wflow
33
using Statistics: mean
44
using Logging: with_logger, NullLogger
55
using Wflow: to_SI, MM
@@ -15,8 +15,8 @@
1515
ZMQ.connect(socket, "tcp://localhost:5555")
1616

1717
function request(message)
18-
ZMQ.send(socket, JSON3.write(message))
19-
ret_value = JSON3.read(ZMQ.recv(socket), Dict; allow_inf = true)
18+
ZMQ.send(socket, JSON.json(message))
19+
ret_value = JSON.parse(ZMQ.recv(socket); dicttype = Dict{String, Any})
2020
return ret_value
2121
end
2222

@@ -33,13 +33,21 @@
3333
@test request((fn = "get_time_units",)) == Dict("time_units" => "s")
3434
end
3535

36-
@testset "Reading and writing NaN values allowed" begin
36+
@testset "non-finite responses encoded as null" begin
37+
# the server encoder maps NaN, Inf and -Inf to null, since JSON cannot represent them.
38+
encoded = WflowServer.serialize_response(Dict("value" => [NaN, Inf, -Inf, 1.0]))
39+
@test !occursin("NaN", encoded)
40+
@test !occursin("Infinity", encoded)
41+
@test occursin("[null,null,null,1.0]", encoded)
42+
@test JSON.parse(encoded)["value"] == [nothing, nothing, nothing, 1.0]
43+
44+
# end-to-end check that a model response containing NaN arrives as null
3745
msg = (
3846
fn = "get_value",
3947
name = "soil_layer_1_water__volume_fraction",
4048
dest = fill(0.0, 50063),
4149
)
42-
@test isnan(mean(request(msg)["value"]))
50+
@test any(isnothing, request(msg)["value"])
4351
end
4452

4553
@testset "update functions" begin

0 commit comments

Comments
 (0)