|
| 1 | +### Serialisation to JSON / string |
| 2 | + |
| 3 | +using JSON: JSON |
| 4 | + |
| 5 | +# String constants for each index type that we support serialisation / |
| 6 | +# deserialisation of |
| 7 | +const _BASE_INTEGER_TYPE = "Base.Integer" |
| 8 | +const _BASE_VECTOR_TYPE = "Base.Vector" |
| 9 | +const _BASE_UNITRANGE_TYPE = "Base.UnitRange" |
| 10 | +const _BASE_STEPRANGE_TYPE = "Base.StepRange" |
| 11 | +const _BASE_ONETO_TYPE = "Base.OneTo" |
| 12 | +const _BASE_COLON_TYPE = "Base.Colon" |
| 13 | +const _CONCRETIZED_SLICE_TYPE = "AbstractPPL.ConcretizedSlice" |
| 14 | +const _BASE_TUPLE_TYPE = "Base.Tuple" |
| 15 | + |
| 16 | +""" |
| 17 | + index_to_dict(::Integer) |
| 18 | + index_to_dict(::AbstractVector{Int}) |
| 19 | + index_to_dict(::UnitRange) |
| 20 | + index_to_dict(::StepRange) |
| 21 | + index_to_dict(::Colon) |
| 22 | + index_to_dict(::ConcretizedSlice{T, Base.OneTo{I}}) where {T, I} |
| 23 | + index_to_dict(::Tuple) |
| 24 | +
|
| 25 | +Convert an index `i` to a dictionary representation. |
| 26 | +""" |
| 27 | +index_to_dict(i::Integer) = Dict("type" => _BASE_INTEGER_TYPE, "value" => i) |
| 28 | +index_to_dict(v::Vector{Int}) = Dict("type" => _BASE_VECTOR_TYPE, "values" => v) |
| 29 | +function index_to_dict(r::UnitRange) |
| 30 | + return Dict("type" => _BASE_UNITRANGE_TYPE, "start" => r.start, "stop" => r.stop) |
| 31 | +end |
| 32 | +function index_to_dict(r::StepRange) |
| 33 | + return Dict( |
| 34 | + "type" => _BASE_STEPRANGE_TYPE, |
| 35 | + "start" => r.start, |
| 36 | + "stop" => r.stop, |
| 37 | + "step" => r.step, |
| 38 | + ) |
| 39 | +end |
| 40 | +function index_to_dict(r::Base.OneTo{I}) where {I} |
| 41 | + return Dict("type" => _BASE_ONETO_TYPE, "stop" => r.stop) |
| 42 | +end |
| 43 | +index_to_dict(::Colon) = Dict("type" => _BASE_COLON_TYPE) |
| 44 | +function index_to_dict(s::ConcretizedSlice{T,R}) where {T,R} |
| 45 | + return Dict("type" => _CONCRETIZED_SLICE_TYPE, "range" => index_to_dict(s.range)) |
| 46 | +end |
| 47 | +function index_to_dict(t::Tuple) |
| 48 | + return Dict("type" => _BASE_TUPLE_TYPE, "values" => map(index_to_dict, t)) |
| 49 | +end |
| 50 | + |
| 51 | +""" |
| 52 | + dict_to_index(dict) |
| 53 | + dict_to_index(symbol_val, dict) |
| 54 | +
|
| 55 | +Convert a dictionary representation of an index `dict` to an index. |
| 56 | +
|
| 57 | +Users can extend the functionality of `dict_to_index` (and hence `VarName` |
| 58 | +de/serialisation) by extending this method along with [`index_to_dict`](@ref). |
| 59 | +Specifically, suppose you have a custom index type `MyIndexType` and you want |
| 60 | +to be able to de/serialise a `VarName` containing this index type. You should |
| 61 | +then implement the following two methods: |
| 62 | +
|
| 63 | +1. `AbstractPPL.index_to_dict(i::MyModule.MyIndexType)` should return a |
| 64 | + dictionary representation of the index `i`. This dictionary must contain the |
| 65 | + key `"type"`, and the corresponding value must be a string that uniquely |
| 66 | + identifies the index type. Generally, it makes sense to use the name of the |
| 67 | + type (perhaps prefixed with module qualifiers) as this value to avoid |
| 68 | + clashes. The remainder of the dictionary can have any structure you like. |
| 69 | +
|
| 70 | +2. Suppose the value of `index_to_dict(i)["type"]` is `"MyModule.MyIndexType"`. |
| 71 | + You should then implement the corresponding method |
| 72 | + `AbstractPPL.dict_to_index(::Val{Symbol("MyModule.MyIndexType")}, dict)`, |
| 73 | + which should take the dictionary representation as the second argument and |
| 74 | + return the original `MyIndexType` object. |
| 75 | +
|
| 76 | +To see an example of this in action, you can look in the the AbstractPPL test |
| 77 | +suite, which contains a test for serialising OffsetArrays. |
| 78 | +""" |
| 79 | +function dict_to_index(dict) |
| 80 | + t = dict["type"] |
| 81 | + if t == _BASE_INTEGER_TYPE |
| 82 | + return dict["value"] |
| 83 | + elseif t == _BASE_VECTOR_TYPE |
| 84 | + return collect(Int, dict["values"]) |
| 85 | + elseif t == _BASE_UNITRANGE_TYPE |
| 86 | + return dict["start"]:dict["stop"] |
| 87 | + elseif t == _BASE_STEPRANGE_TYPE |
| 88 | + return dict["start"]:dict["step"]:dict["stop"] |
| 89 | + elseif t == _BASE_ONETO_TYPE |
| 90 | + return Base.OneTo(dict["stop"]) |
| 91 | + elseif t == _BASE_COLON_TYPE |
| 92 | + return Colon() |
| 93 | + elseif t == _CONCRETIZED_SLICE_TYPE |
| 94 | + return ConcretizedSlice(Base.Slice(dict_to_index(dict["range"]))) |
| 95 | + elseif t == _BASE_TUPLE_TYPE |
| 96 | + return tuple(map(dict_to_index, dict["values"])...) |
| 97 | + else |
| 98 | + # Will error if the method is not defined, but this hook allows users |
| 99 | + # to extend this function |
| 100 | + return dict_to_index(Val(Symbol(t)), dict) |
| 101 | + end |
| 102 | +end |
| 103 | + |
| 104 | +optic_to_dict(::typeof(identity)) = Dict("type" => "identity") |
| 105 | +function optic_to_dict(::PropertyLens{sym}) where {sym} |
| 106 | + return Dict("type" => "property", "field" => String(sym)) |
| 107 | +end |
| 108 | +optic_to_dict(i::IndexLens) = Dict("type" => "index", "indices" => index_to_dict(i.indices)) |
| 109 | +function optic_to_dict(c::ComposedFunction) |
| 110 | + return Dict( |
| 111 | + "type" => "composed", |
| 112 | + "outer" => optic_to_dict(c.outer), |
| 113 | + "inner" => optic_to_dict(c.inner), |
| 114 | + ) |
| 115 | +end |
| 116 | + |
| 117 | +function dict_to_optic(dict) |
| 118 | + if dict["type"] == "identity" |
| 119 | + return identity |
| 120 | + elseif dict["type"] == "index" |
| 121 | + return IndexLens(dict_to_index(dict["indices"])) |
| 122 | + elseif dict["type"] == "property" |
| 123 | + return PropertyLens{Symbol(dict["field"])}() |
| 124 | + elseif dict["type"] == "composed" |
| 125 | + return dict_to_optic(dict["outer"]) ∘ dict_to_optic(dict["inner"]) |
| 126 | + else |
| 127 | + error("Unknown optic type: $(dict["type"])") |
| 128 | + end |
| 129 | +end |
| 130 | + |
| 131 | +function varname_to_dict(vn::VarName) |
| 132 | + return Dict("sym" => getsym(vn), "optic" => optic_to_dict(getoptic(vn))) |
| 133 | +end |
| 134 | + |
| 135 | +function dict_to_varname(dict::Dict{<:AbstractString,Any}) |
| 136 | + return VarName{Symbol(dict["sym"])}(dict_to_optic(dict["optic"])) |
| 137 | +end |
| 138 | + |
| 139 | +""" |
| 140 | + varname_to_string(vn::VarName) |
| 141 | +
|
| 142 | +Convert a `VarName` as a string, via an intermediate dictionary. This differs |
| 143 | +from `string(vn)` in that concretised slices are faithfully represented (rather |
| 144 | +than being pretty-printed as colons). |
| 145 | +
|
| 146 | +For `VarName`s which index into an array, this function will only work if the |
| 147 | +indices can be serialised. This is true for all standard Julia index types, but |
| 148 | +if you are using custom index types, you will need to implement the |
| 149 | +`index_to_dict` and `dict_to_index` methods for those types. See the |
| 150 | +documentation of [`dict_to_index`](@ref) for instructions on how to do this. |
| 151 | +
|
| 152 | +```jldoctest |
| 153 | +julia> varname_to_string(@varname(x)) |
| 154 | +"{\\"optic\\":{\\"type\\":\\"identity\\"},\\"sym\\":\\"x\\"}" |
| 155 | +
|
| 156 | +julia> varname_to_string(@varname(x.a)) |
| 157 | +"{\\"optic\\":{\\"field\\":\\"a\\",\\"type\\":\\"property\\"},\\"sym\\":\\"x\\"}" |
| 158 | +
|
| 159 | +julia> y = ones(2); varname_to_string(@varname(y[:])) |
| 160 | +"{\\"optic\\":{\\"indices\\":{\\"values\\":[{\\"type\\":\\"Base.Colon\\"}],\\"type\\":\\"Base.Tuple\\"},\\"type\\":\\"index\\"},\\"sym\\":\\"y\\"}" |
| 161 | +
|
| 162 | +julia> y = ones(2); varname_to_string(@varname(y[:], true)) |
| 163 | +"{\\"optic\\":{\\"indices\\":{\\"values\\":[{\\"range\\":{\\"stop\\":2,\\"type\\":\\"Base.OneTo\\"},\\"type\\":\\"AbstractPPL.ConcretizedSlice\\"}],\\"type\\":\\"Base.Tuple\\"},\\"type\\":\\"index\\"},\\"sym\\":\\"y\\"}" |
| 164 | +``` |
| 165 | +""" |
| 166 | +varname_to_string(vn::VarName) = JSON.json(varname_to_dict(vn)) |
| 167 | + |
| 168 | +""" |
| 169 | + string_to_varname(str::AbstractString) |
| 170 | +
|
| 171 | +Convert a string representation of a `VarName` back to a `VarName`. The string |
| 172 | +should have been generated by `varname_to_string`. |
| 173 | +""" |
| 174 | +string_to_varname(str::AbstractString) = dict_to_varname(JSON.parse(str)) |
0 commit comments