Skip to content

Commit bccabfc

Browse files
add getkeypath and haskeypath (#76)
* add getkeypath, haskeypath * docs * getindex(x, kp) = getkeypath(x, kp) * remove keypath indexing * use getproperty
1 parent c6ce6e8 commit bccabfc

File tree

5 files changed

+172
-6
lines changed

5 files changed

+172
-6
lines changed

docs/src/api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,6 @@ Functors.IterateWalk
4848

4949
```@docs
5050
Functors.KeyPath
51+
Functors.haskeypath
52+
Functors.getkeypath
5153
```

src/Functors.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
module Functors
22

3-
export @functor, @flexiblefunctor, fmap, fmapstructure, fcollect, execute, fleaves,
4-
KeyPath, fmap_with_path, fmapstructure_with_path
3+
export @functor, @flexiblefunctor, fmap, fmapstructure, fcollect, execute, fleaves,
4+
fmap_with_path, fmapstructure_with_path,
5+
KeyPath, getkeypath, haskeypath
56

67
include("functor.jl")
78
include("keypath.jl")

src/keypath.jl

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Base: tail
2+
13
KeyT = Union{Symbol, AbstractString, Integer}
24

35
"""
@@ -7,14 +9,46 @@ A type for representing a path of keys to a value in a nested structure.
79
Can be constructed with a sequence of keys, or by concatenating other `KeyPath`s.
810
Keys can be of type `Symbol`, `String`, or `Int`.
911
12+
For custom types, access through symbol keys is assumed to be done with `getproperty`.
13+
For consistency, the method `Base.propertynames` is used to get the viable property names.
14+
15+
For string and integer keys instead, the access is done with `getindex`.
16+
17+
See also [`getkeypath`](@ref), [`haskeypath`](@ref).
18+
1019
# Examples
1120
1221
```jldoctest
1322
julia> kp = KeyPath(:b, 3)
1423
KeyPath(:b, 3)
1524
16-
julia> KeyPath(:a, kp, :c, 4)
25+
julia> KeyPath(:a, kp, :c, 4) # construct mixing keys and keypaths
1726
KeyPath(:a, :b, 3, :c, 4)
27+
28+
julia> struct T
29+
a
30+
b
31+
end
32+
33+
julia> function Base.getproperty(x::T, k::Symbol)
34+
if k in fieldnames(T)
35+
return getfield(x, k)
36+
elseif k === :ab
37+
return "ab"
38+
else
39+
error()
40+
end
41+
end;
42+
43+
julia> Base.propertynames(::T) = (:a, :b, :ab);
44+
45+
julia> x = T(3, Dict(:c => 4, :d => 5));
46+
47+
julia> getkeypath(x, KeyPath(:ab)) # equivalent to x.ab
48+
"ab"
49+
50+
julia> getkeypath(x, KeyPath(:b, :c)) # equivalent to (x.b)[:c]
51+
4
1852
```
1953
"""
2054
struct KeyPath{T<:Tuple}
@@ -29,10 +63,14 @@ function KeyPath(keys::Union{KeyT, KeyPath}...)
2963
return KeyPath(((ks...)...,))
3064
end
3165

66+
Base.isempty(kp::KeyPath) = false
67+
Base.isempty(kp::KeyPath{Tuple{}}) = true
3268
Base.getindex(kp::KeyPath, i::Int) = kp.keys[i]
3369
Base.length(kp::KeyPath) = length(kp.keys)
3470
Base.iterate(kp::KeyPath, state=1) = iterate(kp.keys, state)
3571
Base.:(==)(kp1::KeyPath, kp2::KeyPath) = kp1.keys == kp2.keys
72+
Base.tail(kp::KeyPath) = KeyPath(Base.tail(kp.keys))
73+
Base.last(kp::KeyPath) = last(kp.keys)
3674

3775
function Base.show(io::IO, kp::KeyPath)
3876
compat = get(io, :compact, false)
@@ -45,3 +83,72 @@ end
4583

4684
keypathstr(kp::KeyPath) = join(kp.keys, ".")
4785

86+
_getkey(x, k::Integer) = x[k]
87+
_getkey(x, k::Symbol) = getproperty(x, k)
88+
_getkey(x::AbstractDict, k::Symbol) = x[k]
89+
_getkey(x, k::AbstractString) = x[k]
90+
91+
_haskey(x, k::Integer) = haskey(x, k)
92+
_haskey(x::Tuple, k::Integer) = 1 <= k <= length(x)
93+
_haskey(x::AbstractArray, k::Integer) = 1 <= k <= length(x) # TODO: extend to generic indexing
94+
_haskey(x, k::Symbol) = k in propertynames(x)
95+
_haskey(x::AbstractDict, k::Symbol) = haskey(x, k)
96+
_haskey(x, k::AbstractString) = haskey(x, k)
97+
98+
"""
99+
getkeypath(x, kp::KeyPath)
100+
101+
Return the value in `x` at the path `kp`.
102+
103+
See also [`KeyPath`](@ref) and [`haskeypath`](@ref).
104+
105+
# Examples
106+
```jldoctest
107+
julia> x = Dict(:a => 3, :b => Dict(:c => 4, "d" => [5, 6, 7]))
108+
Dict{Symbol, Any} with 2 entries:
109+
:a => 3
110+
:b => Dict{Any, Any}(:c=>4, "d"=>[5, 6, 7])
111+
112+
julia> getkeypath(x, KeyPath(:b, "d", 2))
113+
6
114+
```
115+
"""
116+
function getkeypath(x, kp::KeyPath)
117+
if isempty(kp)
118+
return x
119+
else
120+
return getkeypath(_getkey(x, first(kp)), tail(kp))
121+
end
122+
end
123+
124+
"""
125+
haskeypath(x, kp::KeyPath)
126+
127+
Return `true` if `x` has a value at the path `kp`.
128+
129+
See also [`KeyPath`](@ref) and [`getkeypath`](@ref).
130+
131+
# Examples
132+
```jldoctest
133+
julia> x = Dict(:a => 3, :b => Dict(:c => 4, "d" => [5, 6, 7]))
134+
Dict{Any,Any} with 2 entries:
135+
:a => 3
136+
:b => Dict{Any,Any}(:c=>4,"d"=>[5, 6, 7])
137+
138+
julia> haskeypath(x, KeyPath(:a))
139+
true
140+
141+
julia> haskeypath(x, KeyPath(:b, "d", 1))
142+
true
143+
144+
julia> haskeypath(x, KeyPath(:b, "d", 4))
145+
false
146+
"""
147+
function haskeypath(x, kp::KeyPath)
148+
if isempty(kp)
149+
return true
150+
else
151+
k = first(kp)
152+
return _haskey(x, k) && haskeypath(_getkey(x, k), tail(kp))
153+
end
154+
end

test/basics.jl

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -400,10 +400,13 @@ end
400400
@functor A
401401
a = A(1)
402402
@test Functors.children(a) === (x = 1,)
403-
Functors.@leaf A
404-
children, re = Functors.functor(a)
403+
404+
struct B; x; end
405+
Functors.@leaf B
406+
b = B(1)
407+
children, re = Functors.functor(b)
405408
@test children == Functors.NoChildren()
406-
@test re(children) === a
409+
@test re(children) === b
407410
end
408411

409412
@testset "IterateWalk" begin

test/keypath.jl

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,57 @@
1515

1616
kp0 = KeyPath()
1717
@test (kp0...,) === ()
18+
19+
struct Tkp
20+
a
21+
b
22+
c
23+
end
24+
25+
function Base.getproperty(x::Tkp, k::Symbol)
26+
if k in fieldnames(Tkp)
27+
return getfield(x, k)
28+
elseif k === :ab
29+
return "ab"
30+
else
31+
error()
32+
end
33+
end
34+
35+
Base.propertynames(::Tkp) = (:a, :b, :c, :ab)
36+
37+
@testset "getkeypath" begin
38+
x = Dict(:a => 3, :b => Dict(:c => 4, "d" => [5, 6, 7]))
39+
@test getkeypath(x, KeyPath(:a)) == 3
40+
@test getkeypath(x, KeyPath(:b, :c)) == 4
41+
@test getkeypath(x, KeyPath(:b, "d", 2)) == 6
42+
43+
x = Tkp(3, Tkp(4, 5, (6, 7)), 8)
44+
kp = KeyPath(:b, :c, 2)
45+
@test getkeypath(x, kp) == 7
46+
47+
@testset "access through getproperty" begin
48+
x = Tkp(3, Dict(:c => 4, :d => 5), 6);
49+
50+
@test getkeypath(x, KeyPath(:ab)) == "ab"
51+
@test getkeypath(x, KeyPath(:b, :c)) == 4
52+
end
53+
end
54+
55+
@testset "haskeypath" begin
56+
x = Dict(:a => 3, :b => Dict(:c => 4, "d" => [5, 6, 7]))
57+
@test haskeypath(x, KeyPath(:a))
58+
@test haskeypath(x, KeyPath(:b, :c))
59+
@test haskeypath(x, KeyPath(:b, "d", 2))
60+
@test !haskeypath(x, KeyPath(:b, "d", 4))
61+
@test !haskeypath(x, KeyPath(:b, "e"))
62+
63+
@testset "access through getproperty" begin
64+
x = Tkp(3, Dict(:c => 4, :d => 5), 6);
65+
66+
@test haskeypath(x, KeyPath(:ab))
67+
@test haskeypath(x, KeyPath(:b, :c))
68+
@test !haskeypath(x, KeyPath(:b, :e))
69+
end
70+
end
1871
end

0 commit comments

Comments
 (0)