< Summary

Class:src/pointer.jl
Assembly:Default
File(s):src/pointer.jl
Covered lines:118
Uncovered lines:9
Coverable lines:127
Total lines:312
Line coverage:92.9% (118 of 127)
Covered branches:0
Total branches:0
Tag:27_719686382

Coverage History

File(s)

src/pointer.jl

#LineLine coverage
 1const TOKEN_PREFIX = '/'
 2
 3macro j_str(token)
 944    Pointer(token)
 5end
 6
 7"""
 8    _unescape_jpath(raw::String)
 9
 10Transform escaped characters in JPaths back to their original value.
 11https://tools.ietf.org/html/rfc6901
 12"""
 13function _unescape_jpath(raw::AbstractString)
 1314    m = match(r"%([0-9A-F]{2})", raw)
 1315    if m !== nothing
 616        for c in m.captures
 1217            raw = replace(raw, "%$(c)" => Char(parse(UInt8, "0x$(c)")))
 18        end
 19    end
 1320    return raw
 21end
 22
 23function _last_element_to_type!(jk)
 9724    if !occursin("::", jk[end])
 8625        return Any
 26    end
 1127    x = split(jk[end], "::")
 1128    jk[end] = String(x[1])
 1129    if x[2] == "string"
 130        return String
 1031    elseif x[2] == "number"
 132        return Union{Int, Float64}
 933    elseif x[2] == "object"
 234        return OrderedCollections.OrderedDict{String, Any}
 735    elseif x[2] == "array"
 236        return Vector{Any}
 537    elseif x[2] == "boolean"
 238        return Bool
 339    elseif x[2] == "null"
 140        return Missing
 41    else
 242        error(
 43            "You specified a type that JSON doesn't recognize! Instead of " *
 44            "`::$(x[2])`, you must use one of `::string`, `::number`, " *
 45            "`::object`, `::array`, `::boolean`, or `::null`."
 46        )
 47    end
 48end
 49
 50"""
 51    Pointer(token::AbstractString; shift_index::Bool = false)
 52
 53A JSON Pointer is a Unicode string containing a sequence of zero or more
 54reference tokens, each prefixed by a '/' (%x2F) character.
 55
 56Follows IETF JavaScript Object Notation (JSON) Pointer https://tools.ietf.org/html/rfc6901.
 57
 58## Arguments
 59
 60- `shift_index`: shift given index by 1 for compatibility with original JSONPointer.
 61
 62## Non-standard extensions
 63
 64- Index numbers starts from `1` instead of `0`
 65
 66- User can declare type with '::T' notation at the end. For example
 67  `/foo::string`. The type `T` must be one of the six types supported by JSON:
 68  * `::string`
 69  * `::number`
 70  * `::object`
 71  * `::array`
 72  * `::boolean`
 73  * `::null`
 74
 75## Examples
 76
 77    Pointer("/a")
 78    Pointer("/a/3")
 79    Pointer("/a/b/c::number")
 80    Pointer("/a/0/c::object"; shift_index = true)
 81"""
 82struct Pointer{T}
 10583    tokens::Vector{Union{String, Int}}
 84end
 85
 86function Pointer(token_string::AbstractString; shift_index::Bool = false)
 31087    if startswith(token_string, "#")
 2588        token_string = _unescape_jpath(token_string[2:end])
 89    end
 10490    if isempty(token_string)
 391        return Pointer{Nothing}([""])
 92    end
 20293    if !startswith(token_string, TOKEN_PREFIX)
 194        throw(ArgumentError("JSONPointer must starts with '$TOKEN_PREFIX' prefix"))
 95    end
 19796    tokens = convert(
 97        Vector{Union{String, Int}},
 98        String.(split(token_string, TOKEN_PREFIX; keepempty = false)),
 99    )
 100100    if length(tokens) == 0
 3101        return Pointer{Any}([""])
 102    end
 97103    T = _last_element_to_type!(tokens)
 190104    for (i, token) in enumerate(tokens)
 217105        if occursin(r"^\d+$", token) # index of a array
 51106            tokens[i] = parse(Int, token)
 51107            if shift_index
 3108                tokens[i] += 1
 109            end
 51110            if iszero(tokens[i])
 2111                throw(ArgumentError("Julia uses 1-based indexing, use '1' instead of '0'"))
 112            end
 166113        elseif occursin(r"^\\\d+$", token) # literal string for a number
 3114            tokens[i] = String(chop(token; head = 1, tail = 0))
 163115        elseif occursin("~", token)
 343116            tokens[i] = replace(replace(token, "~0" => "~"), "~1" => "/")
 117        end
 118    end
 93119    return Pointer{T}(tokens)
 120end
 121
 66122Base.length(x::Pointer) = length(x.tokens)
 123
 16124Base.eltype(::Pointer{T}) where {T} = T
 125
 0126function Base.show(io::IO, x::Pointer{T}) where {T}
 0127    print(io, "JSONPointer{", T, "}(\"/", join(x.tokens, "/"), "\")")
 128end
 129
 0130function Base.show(io::IO, ::Pointer{Nothing})
 0131    print(io, "JSONPointer{Nothing}(\"\")")
 132end
 133
 134# This code block needs some explaining.
 135#
 136# Ideally, one would define methods like Base.haskey(::AbstractDict, ::Pointer).
 137# However, this causes an ambiguity with Base.haskey(::Dict, key), which has a
 138# more concrete first argument and a less concrete second argument. We could
 139# just define both methods to avoid the ambiguity with Dict, but this would
 140# probably break any package which defines an <:AbstractDict and fails to type
 141# the second argument to haskey, getindex, etc!
 142#
 143# To avoid the ambiguity issue, we have to manually encode each AbstractDict
 144# subtype that we support :(
 145for T in (Dict, OrderedCollections.OrderedDict)
 146    @eval begin
 147        # This method is used when creating new dictionaries from JSON pointers.
 148        function $T{K, V}(kv::Pair{<:Pointer, V}...) where {V, K<:Pointer}
 1149            return $T{String, Any}()
 150        end
 151
 8152        _new_container(::$T) = $T{String, Any}()
 153
 20154        Base.haskey(dict::$T, p::Pointer) = _haskey(dict, p)
 51155        Base.getindex(dict::$T, p::Pointer) = _getindex(dict, p)
 21156        Base.setindex!(dict::$T, v, p::Pointer) = _setindex!(dict, v, p)
 3157        Base.get(dict::$T, p::Pointer, default) = _get(dict, p, default)
 158    end
 159end
 160
 7161Base.getindex(A::AbstractArray, p::Pointer) = _getindex(A, p)
 2162Base.haskey(A::AbstractArray, p::Pointer) = _haskey(A, p)
 163
 164function Base.unique(arr::AbstractArray{<:Pointer, N}) where {N}
 1165    out = deepcopy(arr)
 1166    if isempty(arr)
 0167        return out
 168    end
 2169    pointers = getfield.(arr, :tokens)
 1170    if allunique(pointers)
 0171        return out
 172    end
 1173    delete_target = Int[]
 1174    for p in pointers
 3175        indicies = findall(el -> el == p, pointers)
 3176        if length(indicies) > 1
 3177            append!(delete_target, indicies[1:end-1])
 178        end
 179    end
 1180    deleteat!(out, unique(delete_target))
 1181    return out
 182end
 183
 1184Base.:(==)(a::Pointer{U}, b::Pointer{U}) where {U} = a.tokens == b.tokens
 185
 186# ==============================================================================
 187
 41188_checked_get(collection::AbstractArray, token::Int) = collection[token]
 189
 156190_checked_get(collection::AbstractDict, token::String) = collection[token]
 191
 192function _checked_get(collection, token)
 1193    error(
 194        "JSON pointer does not match the data-structure. I tried (and " *
 195        "failed) to index $(collection) with the key: $(token)"
 196    )
 197end
 198
 199# ==============================================================================
 200
 1201_haskey(::Any, ::Pointer{Nothing}) = true
 202
 203function _haskey(collection, p::Pointer)
 24204    for token in p.tokens
 132205        if !_haskey(collection, token)
 6206            return false
 207        end
 79208        collection = _checked_get(collection, token)
 209    end
 18210    return true
 211end
 212
 57213_haskey(collection::AbstractDict, token::String) = haskey(collection, token)
 214
 215function _haskey(collection::AbstractArray, token::Int)
 10216    return 1 <= token <= length(collection)
 217end
 218
 0219_haskey(::Any, ::Any) = false
 220
 221# ==============================================================================
 222
 2223_getindex(collection, ::Pointer{Nothing}) = collection
 224
 225function _getindex(collection, p::Pointer)
 57226    return _getindex(collection, p.tokens)
 227end
 228
 229function _getindex(collection, tokens::Vector{Union{String, Int}})
 57230    for token in tokens
 189231        collection = _checked_get(collection, token)
 232    end
 52233    return collection
 234end
 235
 236# ==============================================================================
 237
 238function _get(collection, p::Pointer, default)
 3239    if _haskey(collection, p)
 1240        return _getindex(collection, p)
 241    end
 2242    return default
 243end
 244
 245# ==============================================================================
 246
 5247_null_value(p::Pointer) = _null_value(eltype(p))
 1248_null_value(::Type{String}) = ""
 1249_null_value(::Type{<:Real}) = 0
 1250_null_value(::Type{<:AbstractDict}) = OrderedCollections.OrderedDict{String, Any}()
 1251_null_value(::Type{<:AbstractVector{T}}) where {T} = T[]
 1252_null_value(::Type{Bool}) = false
 0253_null_value(::Type{Nothing}) = nothing
 0254_null_value(::Type{Missing}) = missing
 255
 24256_null_value(::Type{Any}) = missing
 257
 17258_convert_v(v::U, ::Pointer{U}) where {U} = v
 259function _convert_v(v::V, p::Pointer{U}) where {U, V}
 8260    v = ismissing(v) ? _null_value(p) : v
 8261    try
 8262        return convert(eltype(p), v)
 263    catch
 2264        error(
 265            "$(v)::$(typeof(v)) is not valid type for $(p). Remove type " *
 266            "assertion in the JSON pointer if you don't a need static type."
 267        )
 268    end
 269end
 270
 271function _add_element_if_needed(prev::AbstractVector{T}, k::Int) where {T}
 22272    x = k - length(prev)
 22273    if x > 0
 22274        append!(prev, [_null_value(T) for _ = 1:x])
 275    end
 22276    return
 277end
 278
 279function _add_element_if_needed(
 280    prev::AbstractDict{K, V}, k::String
 281) where {K, V}
 44282    if !haskey(prev, k)
 25283        prev[k] = _null_value(V)
 284    end
 285end
 286
 287function _add_element_if_needed(collection, token)
 3288    error(
 289        "JSON pointer does not match the data-structure. I tried (and " *
 290        "failed) to set $(collection) at the index: $(token)"
 291    )
 292end
 293
 9294_new_data(::Any, n::Int) = Vector{Any}(missing, n)
 4295_new_data(::AbstractVector, ::String) = OrderedCollections.OrderedDict{String, Any}()
 8296_new_data(x::AbstractDict, ::String) = _new_container(x)
 297
 298function _setindex!(collection::AbstractDict, v, p::Pointer)
 28299    prev = collection
 56300    for (i, token) in enumerate(p.tokens)
 71301        _add_element_if_needed(prev, token)
 66302        if i != length(p)
 61303            if ismissing(prev[token])
 21304                prev[token] = _new_data(prev, p.tokens[i + 1])
 305            end
 82306            prev = prev[token]
 307        end
 308    end
 25309    prev[p.tokens[end]] = _convert_v(v, p)
 23310    return v
 311end
 312

Methods/Properties