diff --git a/Project.toml b/Project.toml index 3b3ebd2..431fdbd 100644 --- a/Project.toml +++ b/Project.toml @@ -1,4 +1,4 @@ -name = "DotMap" +name = "DotMaps" uuid = "98454f44-f182-4945-8d34-ddde53e72162" authors = ["mcmcgrath13 and contributors"] version = "0.1.0" diff --git a/README.md b/README.md index 6825ade..c388b67 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ -# DotMap +# DotMaps -[![Build Status](https://github.com/mcmcgrath13/DotMap.jl/workflows/CI/badge.svg)](https://github.com/mcmcgrath13/DotMap.jl/actions) +[![Build Status](https://github.com/mcmcgrath13/DotMaps.jl/workflows/CI/badge.svg)](https://github.com/mcmcgrath13/DotMaps.jl/actions) + +A wrapper for dictionaries that allows dot notation indexing as well as traditional bracket indexing. + +```julia +dict = Dict("a"=>1, "b"=>2, "c" => Dict("d"=>3)) +dm = DotMap(dict) + +dm.c.d # returns 3 +dm.c.e = 5 +dm["c"].e # returns 5 +``` + +**NOTE** This is not as performative as using normal dictionaries, but is nice for accessing deeply nested dictionary structures, such as large config/yaml/json files. diff --git a/src/DotMap.jl b/src/DotMap.jl deleted file mode 100644 index b387850..0000000 --- a/src/DotMap.jl +++ /dev/null @@ -1,5 +0,0 @@ -module DotMap - -# Write your package code here. - -end diff --git a/src/DotMaps.jl b/src/DotMaps.jl new file mode 100644 index 0000000..508624b --- /dev/null +++ b/src/DotMaps.jl @@ -0,0 +1,82 @@ +module DotMaps + +export DotMap + +""" + DotMaps.DotMap(::Dict) + +Constructs a DotMap from a Dict. This provides the same functionaliity as dictionaries, but allows indexing with `.` instead of (or in addition to) `[""]`. +""" +mutable struct DotMap + __dict__ ::Dict{Symbol,Any} + DotMap() = new(Dict{Symbol,Any}()) +end + +function DotMap(d::Dict) + dm = DotMap() + for (k, v) in d + dm.__dict__[Symbol(k)] = DotMap(v) + end + return dm +end + +DotMap(d::Any) = d + +# make dots work +function Base.getproperty(obj::DotMap, name::Symbol) + if name == :__dict__ + return getfield(obj, name) + else + return obj.__dict__[name] + end +end + +function Base.setproperty!(obj::DotMap, name::Symbol, x) + obj.__dict__[name] = x +end + +# make dictionary indexing work +Base.getindex(obj::DotMap, name::Symbol) = Base.getindex(obj.__dict__, name) +Base.getindex(obj::DotMap, name::Any) = Base.getindex(obj, Symbol(name)) + +Base.setindex!(obj::DotMap, name::Symbol, x) = Base.setindex!(obj.__dict__, name, x) +Base.setindex!(obj::DotMap, name, x) = Base.setindex!(obj, Symbol(name), x) + +# iteration +Base.iterate(obj::DotMap) = Base.iterate(obj.__dict__) +Base.iterate(obj::DotMap, s::Any) = Base.iterate(obj.__dict__, s) +Base.length(obj::DotMap) = Base.length(obj.__dict__) +Base.firstindex(obj::DotMap) = Base.firstindex(obj.__dict__) +Base.lastindex(obj::DotMap) = Base.lastindex(obj.__dict__) + +# dictionary methods +Base.keys(obj::DotMap) = Base.keys(obj.__dict__) +Base.values(obj::DotMap) = Base.values(obj.__dict__) +Base.collect(obj::DotMap) = Base.collect(obj.__dict__) + +# retrieval/modification +Base.get(obj::DotMap, key::Symbol, default) = Base.get(obj.__dict__, key, default) +Base.get(obj::DotMap, key::Any, default) = Base.get(obj.__dict__, Symbol(key), default) +Base.get!(obj::DotMap, key::Symbol, default) = Base.get!(obj.__dict__, key, default) +Base.get!(obj::DotMap, key::Any, default) = Base.get!(obj.__dict__, Symbol(key), default) +Base.getkey(obj::DotMap, key::Symbol, default) = Base.getkey(obj.__dict__, key, default) +Base.getkey(obj::DotMap, key::Any, default) = Base.getkey(obj.__dict__, Symbol(key), default) +Base.pop!(obj::DotMap, key::Symbol) = Base.pop!(obj.__dict__, key) +Base.pop!(obj::DotMap, key::Any) = Base.pop!(obj.__dict__, Symbol(key)) +Base.delete!(obj::DotMap, key::Symbol) = Base.pop!(obj.__dict__, key) +Base.delete!(obj::DotMap, key::Any) = Base.pop!(obj.__dict__, Symbol(key)) +Base.filter!(pred, obj::DotMap) = Base.filter!(pred, obj.__dict__) +Base.filter(pred, obj::DotMap) = DotMap(Base.filter(pred, obj.__dict__)) + +# about this dict +Base.propertynames(obj::DotMap) = Base.keys(obj.__dict__) +Base.isempty(obj::DotMap) = Base.isempty(obj.__dict__) +Base.show(obj::DotMap) = Base.show(obj.__dict__) + +# containment +Base.in(key::Symbol, obj::DotMap) = Base.in(key, obj.__dict__) +Base.in(key::Any, obj::DotMap) = Base.in(Symbol(key), obj.__dict__) +Base.haskey(obj::DotMap, key::Symbol) = Base.haskey(obj.__dict__, key) +Base.haskey(obj::DotMap, key::Any) = Base.haskey(obj.__dict__, Symbol(key)) + +end diff --git a/test/runtests.jl b/test/runtests.jl index ddf38d5..ec6a5fb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,42 @@ -using DotMap +using DotMaps using Test -@testset "DotMap.jl" begin - # Write your tests here. +@testset "DotMaps.jl" begin + + dict = Dict("a"=>1, "b"=>2, "c" => Dict("d"=>3)) + DM = DotMap(dict) + + @test DM.a == 1 + @test DM.c.d == 3 + + DM.c.e = 4 + @test DM.c.e == 4 + + delete!(DM.c, "e") + @test !("e" in keys(DM.c)) + + @test get!(DM.c, "e", 5) == 5 + @test DM.c.e == 5 + @test DM["c"].e == 5 + + for (k, v) in DM + @test isa(k, Symbol) + end + + @test length(DM) == 3 + @test 3 in collect(values(DM.c)) + + @test pop!(DM.c, "e") == 5 + + filtered = filter(x -> isa(last(x), Int), DM) + @test !("c" in keys(filtered)) + @test filtered.a == 1 + + filter!(x -> isa(last(x), Int), DM) + @test !("c" in keys(DM)) + @test haskey(DM, "a") + @test DM.a == 1 + @test :a in propertynames(DM) + + @test isempty(DotMap()) end