Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions vendor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ A simple, no-dependency JSON parser that can be vendored (copied/pasted) into ot
**Reading (JSON → Julia):**
- `null` → `nothing`
- `true`/`false` → `Bool`
- Numbers → `Float64` (all numbers are parsed as Float64)
- Numbers → `Int64` or `Float64` (integers without decimal/exponent → Int64, with overflow fallback to Float64)
- Strings → `String` (with full Unicode support)
- Arrays → `Vector{Any}`
- Objects → `Dict{String, Any}`
Expand Down Expand Up @@ -80,7 +80,7 @@ JSONX provides detailed error messages for invalid JSON:

Compared to the full JSON.jl package, JSONX is intentionally simplified:

- **No integer parsing**: All numbers are parsed as Float64
- **Limited numeric types**: Parses as Int64 or Float64 only (no BigInt/BigFloat for very large numbers)
- **No custom type parsing**: Only returns basic Julia types
- **No configuration options**: Uses fixed defaults
- **No streaming**: Loads entire input into memory
Expand Down
31 changes: 26 additions & 5 deletions vendor/jsonx.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ module JSONX
JSONX.parse(bytes::AbstractVector{UInt8})

Parse a JSON string or byte array and return a Julia value.
Returns one of: Dict{String, Any}, Vector{Any}, String, Float64, Bool, or Nothing.
Returns one of: Dict{String, Any}, Vector{Any}, String, Int64, Float64, Bool, or Nothing.
Numbers without decimal points or exponents are parsed as Int64, falling back to Float64 on overflow.
"""
function parse(json_str::String)
pos = 1
Expand Down Expand Up @@ -251,10 +252,30 @@ end

function parse_number(str::String, pos::Int, len::Int)
start_pos = pos
while pos <= len && (codeunit(str, pos) == UInt8('-') || (UInt8('0') <= codeunit(str, pos) <= UInt8('9')) || codeunit(str, pos) == UInt8('.') || codeunit(str, pos) == UInt8('e') || codeunit(str, pos) == UInt8('E') || codeunit(str, pos) == UInt8('+'))
pos += 1
has_decimal_or_exp = false
while pos <= len
c = codeunit(str, pos)
if c == UInt8('-') || (UInt8('0') <= c <= UInt8('9')) || c == UInt8('+')
pos += 1
elseif c == UInt8('.') || c == UInt8('e') || c == UInt8('E')
has_decimal_or_exp = true
pos += 1
else
break
end
end
num_str = str[start_pos:pos-1]
num_str = @view str[start_pos:pos-1]

# Try parsing as Int64 if no decimal point or exponent
if !has_decimal_or_exp
try
return Base.parse(Int64, num_str), pos
catch
# Fall back to Float64 if Int64 parsing fails (e.g., overflow)
end
end

# Parse as Float64
try
return Base.parse(Float64, num_str), pos
catch
Expand Down Expand Up @@ -339,4 +360,4 @@ function write_object(io::IO, dict::Union{AbstractDict, NamedTuple})
print(io, '}')
end

end # module
end # module
90 changes: 59 additions & 31 deletions vendor/test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ include("jsonx.jl")
# Test booleans
@test JSONX.parse("true") === true
@test JSONX.parse("false") === false
# Test numbers (all return Float64)
@test JSONX.parse("0") == 0.0
@test JSONX.parse("123") == 123.0
@test JSONX.parse("-123") == -123.0
# Test integers (return Int64)
@test JSONX.parse("0") === Int64(0)
@test JSONX.parse("123") === Int64(123)
@test JSONX.parse("-123") === Int64(-123)
# Test floats (return Float64)
@test JSONX.parse("3.14") == 3.14
@test JSONX.parse("-3.14") == -3.14
@test JSONX.parse("1e2") == 100.0
Expand All @@ -27,40 +28,67 @@ include("jsonx.jl")
@test JSONX.parse("\"\\\\backslash\\\\\"") == "\\backslash\\"
@test JSONX.parse("\"\\n\\t\\r\\b\\f\"") == "\n\t\r\b\f"
end

@testset "Integer vs Float Parsing" begin
# Integers without decimal/exponent -> Int64
@test JSONX.parse("42") === Int64(42)
@test JSONX.parse("-100") === Int64(-100)
@test JSONX.parse("0") === Int64(0)

# Numbers with decimal point -> Float64
@test JSONX.parse("42.0") === 42.0
@test JSONX.parse("1.5") === 1.5

# Numbers with exponent -> Float64
@test JSONX.parse("1e10") === 1e10
@test JSONX.parse("1E5") === 1e5
@test JSONX.parse("2.5e3") === 2500.0
@test JSONX.parse("1e10") isa Float64

# Int64 overflow fallback to Float64
@test JSONX.parse("99999999999999999999") == 1e20

# Negative overflow
@test JSONX.parse("-99999999999999999999") == -1e20

# Edge case: numbers at Int64 boundary
@test JSONX.parse(string(typemax(Int64))) === typemax(Int64)
@test JSONX.parse(string(typemin(Int64))) === typemin(Int64)
end

@testset "Arrays" begin
# Test empty array
@test JSONX.parse("[]") == []
# Test simple arrays
@test JSONX.parse("[1,2,3]") == [1.0, 2.0, 3.0]
@test JSONX.parse("[1,2,3]") == [1, 2, 3]
@test JSONX.parse("[\"a\",\"b\",\"c\"]") == ["a", "b", "c"]
@test JSONX.parse("[true,false,null]") == [true, false, nothing]
# Test nested arrays
@test JSONX.parse("[[1,2],[3,4]]") == [[1.0, 2.0], [3.0, 4.0]]
@test JSONX.parse("[1,[2,3],4]") == [1.0, [2.0, 3.0], 4.0]
@test JSONX.parse("[[1,2],[3,4]]") == [[1, 2], [3, 4]]
@test JSONX.parse("[1,[2,3],4]") == [1, [2, 3], 4]
# Test mixed types
@test JSONX.parse("[1,\"two\",3.0,true,null]") == [1.0, "two", 3.0, true, nothing]
@test JSONX.parse("[1,\"two\",3.0,true,null]") == [1, "two", 3.0, true, nothing]
end

@testset "Objects" begin
# Test empty object
@test JSONX.parse("{}") == Dict{String, Any}()
# Test simple objects
@test JSONX.parse("{\"key\":\"value\"}") == Dict("key" => "value")
@test JSONX.parse("{\"a\":1,\"b\":2}") == Dict("a" => 1.0, "b" => 2.0)
@test JSONX.parse("{\"a\":1,\"b\":2}") == Dict("a" => 1, "b" => 2)
# Test nested objects
@test JSONX.parse("{\"a\":{\"b\":\"c\"}}") == Dict("a" => Dict("b" => "c"))
@test JSONX.parse("{\"a\":[1,2,3]}") == Dict("a" => [1.0, 2.0, 3.0])
@test JSONX.parse("{\"a\":[1,2,3]}") == Dict("a" => [1, 2, 3])
# Test mixed types
@test JSONX.parse("{\"str\":\"hello\",\"num\":123,\"bool\":true,\"null\":null}") ==
Dict("str" => "hello", "num" => 123.0, "bool" => true, "null" => nothing)
@test JSONX.parse("{\"str\":\"hello\",\"num\":123,\"bool\":true,\"null\":null}") ==
Dict("str" => "hello", "num" => 123, "bool" => true, "null" => nothing)
end

@testset "Whitespace Handling" begin
# Test various whitespace
@test JSONX.parse(" null ") === nothing
@test JSONX.parse("\t\n\r null \t\n\r") === nothing
@test JSONX.parse("[ 1 , 2 , 3 ]") == [1.0, 2.0, 3.0]
@test JSONX.parse("[ 1 , 2 , 3 ]") == [1, 2, 3]
@test JSONX.parse("{ \"key\" : \"value\" }") == Dict("key" => "value")
end

Expand Down Expand Up @@ -100,15 +128,15 @@ include("jsonx.jl")
@test JSONX.json(Dict{String, Any}()) == "{}"
@test JSONX.json(Dict("key" => "value")) == "{\"key\":\"value\"}"
# Note: Dictionary order is not guaranteed, so we parse and compare
@test JSONX.parse(JSONX.json(Dict("a" => 1, "b" => 2))) == Dict("a" => 1.0, "b" => 2.0)
@test JSONX.parse(JSONX.json(Dict("a" => 1, "b" => 2))) == Dict("a" => 1, "b" => 2)
# Test nested structures
@test JSONX.json(Dict("a" => Dict("b" => "c"))) == "{\"a\":{\"b\":\"c\"}}"
@test JSONX.json(Dict("a" => [1, 2, 3])) == "{\"a\":[1,2,3]}"
# Test other types
@test JSONX.json(:symbol) == "\"symbol\""
@test JSONX.json((1, 2, 3)) == "[1,2,3]"
# Note: NamedTuple order is not guaranteed, so we parse and compare
@test JSONX.parse(JSONX.json((a=1, b=2))) == Dict("a" => 1.0, "b" => 2.0)
@test JSONX.parse(JSONX.json((a=1, b=2))) == Dict("a" => 1, "b" => 2)
# Test JSONText
@test JSONX.json(JSONX.JSONText("{\"x\": invalid json}")) == "{\"x\": invalid json}"
end
Expand Down Expand Up @@ -143,25 +171,25 @@ include("jsonx.jl")
nothing,
true,
false,
0.0,
123.0,
-123.0,
0,
123,
-123,
3.14,
-3.14,
"",
"hello",
"quoted \"string\"",
[],
[1.0, 2.0, 3.0],
[1, 2, 3],
["a", "b", "c"],
[1.0, "mixed", 3.0, true, nothing],
[1, "mixed", 3.0, true, nothing],
Dict{String, Any}(),
Dict("key" => "value"),
Dict("a" => 1.0, "b" => 2.0),
Dict("a" => 1, "b" => 2),
Dict("nested" => Dict("key" => "value")),
Dict("array" => [1.0, 2.0, 3.0]),
Dict("array" => [1, 2, 3]),
]

for case in test_cases
json_str = JSONX.json(case)
parsed = JSONX.parse(json_str)
Expand All @@ -171,7 +199,7 @@ include("jsonx.jl")

@testset "Edge Cases" begin
# Test very large numbers
@test JSONX.parse("1234567890123456789") == 1234567890123456789.0
@test JSONX.parse("1234567890123456789") === 1234567890123456789
@test JSONX.parse("1.234567890123456789") == 1.234567890123456789
# Test scientific notation
@test JSONX.parse("1e10") == 1e10
Expand All @@ -198,9 +226,9 @@ include("jsonx.jl")
"""
expected = Dict(
"name" => "John",
"age" => 30.0,
"age" => 30,
"active" => true,
"scores" => [85.0, 92.0, 78.0],
"scores" => [85, 92, 78],
"address" => Dict("street" => "123 Main St", "city" => "Anytown"),
"tags" => ["developer", "programmer"],
"metadata" => nothing
Expand All @@ -221,10 +249,10 @@ include("jsonx.jl")

@testset "AbstractVector{UInt8} Support" begin
@test JSONX.parse(Vector{UInt8}("null")) === nothing
@test JSONX.parse(Vector{UInt8}("42")) == 42.0
@test JSONX.parse(Vector{UInt8}("42")) === 42
@test JSONX.parse(Vector{UInt8}("\"hello\"")) == "hello"
@test JSONX.parse(Vector{UInt8}("[1,2,3]")) == [1.0, 2.0, 3.0]
@test JSONX.parse(Vector{UInt8}("{\"a\":1}")) == Dict("a" => 1.0)
@test JSONX.parse(Vector{UInt8}("[1,2,3]")) == [1, 2, 3]
@test JSONX.parse(Vector{UInt8}("{\"a\":1}")) == Dict("a" => 1)
end

@testset "Unicode Handling" begin
Expand Down Expand Up @@ -292,7 +320,7 @@ include("jsonx.jl")
write(test_file, "{\"test\":\"value\",\"number\":42}")
# Test parsefile
result = JSONX.parsefile(test_file)
@test result == Dict("test" => "value", "number" => 42.0)
@test result == Dict("test" => "value", "number" => 42)
# Test with Unicode content
write(test_file, "\"Hello 世界! 🌍\"")
result = JSONX.parsefile(test_file)
Expand All @@ -304,4 +332,4 @@ include("jsonx.jl")
end
end

println("All JSONX tests passed!")
println("All JSONX tests passed!")
Loading