diff --git a/Project.toml b/Project.toml index 0b21a6b..2897405 100644 --- a/Project.toml +++ b/Project.toml @@ -1,3 +1,12 @@ name = "EnumX" uuid = "4e289a0a-7415-4d19-859d-a7e5c4648b56" version = "1.0.0" + +[compat] +julia = "1.6" + +[extras] +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Test"] diff --git a/README.md b/README.md index 910481e..6289881 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ # EnumX + +`Base.@enum`s protected by a module scope. diff --git a/src/EnumX.jl b/src/EnumX.jl index dbc8d51..2886e94 100644 --- a/src/EnumX.jl +++ b/src/EnumX.jl @@ -2,4 +2,62 @@ module EnumX +export @enumx + +abstract type Enum{T} <: Base.Enum{T} end + +macro enumx(args...) + return enumx(args...) +end + +function enumx(name, args...) + namemap = Dict{Int32,Symbol}() + next = 0 + for arg in args + @assert arg isa Symbol # TODO + namemap[next] = arg + next += 1 + end + module_block = quote + primitive type Type <: Enum{Int32} 32 end + let namemap = $(namemap) + check_valid(x) = x in keys(namemap) || + throw(ArgumentError("invalid value $(x) for Enum $($(QuoteNode(name)))")) + global function $(esc(:Type))(x::Integer) + check_valid(x) + return Base.bitcast($(esc(:Type)), convert(Int32, x)) + end + Base.Enums.namemap(::Base.Type{$(esc(:Type))}) = namemap + Base.Enums.instances(::Base.Type{$(esc(:Type))}) = + ($([esc(k) for k in values(namemap)]...),) + end + end + for (k, v) in namemap + push!(module_block.args, + Expr(:const, Expr(:(=), esc(v), Expr(:call, esc(:Type), k))) + ) + end + return Expr(:toplevel, Expr(:module, false, esc(name), module_block), nothing) +end + +function Base.show(io::IO, ::MIME"text/plain", x::E) where E <: Enum + print(io, "$(nameof(parentmodule(E))).$(Symbol(x)::Symbol) = $(Integer(x))") + return nothing +end +function Base.show(io::IO, ::MIME"text/plain", ::Base.Type{E}) where E <: Enum + iob = IOBuffer() + insts = Base.Enums.instances(E) + n = length(insts) + stringmap = Dict{String, Int32}( + string("$(nameof(parentmodule(E))).", v) => k for (k, v) in Base.Enums.namemap(E) + ) + mx = maximum(textwidth, keys(stringmap); init = 0) + print(iob, "Enum type $(nameof(parentmodule(E))).Type <: Enum{$(Base.Enums.basetype(E))} with $(n) instance$(n == 1 ? "" : "s"):") + for (k, v) in stringmap + print(iob, "\n", rpad(k, mx), " = $(v)") + end + write(io, seekstart(iob)) + return nothing +end + end # module EnumX diff --git a/test/runtests.jl b/test/runtests.jl index c400ea2..f78ba98 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,4 +4,68 @@ using EnumX, Test @testset "EnumX" begin +# Basic +@enumx Fruit Apple Banana + +@test Fruit isa Module +@test Set(names(Fruit)) == Set([:Fruit]) +@test_broken Set(names(Fruit; all=true)) == Set([:Fruit, :Apple, :Banana, :Type]) +@test issubset(Set([:Fruit, :Apple, :Banana, :Type]), Set(names(Fruit; all=true))) +@test Fruit.Type <: EnumX.Enum{Int32} <: Base.Enum{Int32} +@test !@isdefined(Apple) +@test !@isdefined(Banana) + +@test Fruit.Apple isa EnumX.Enum +@test Fruit.Apple isa Base.Enum +@test Fruit.Banana isa EnumX.Enum +@test Fruit.Banana isa Base.Enum + +@test instances(Fruit.Type) === (Fruit.Apple, Fruit.Banana) +@test Base.Enums.namemap(Fruit.Type) == Dict{Int32,Symbol}(0 => :Apple, 1 => :Banana) +@test Base.Enums.basetype(Fruit.Type) == Int32 + +@test Symbol(Fruit.Apple) === :Apple +@test Symbol(Fruit.Banana) === :Banana + +@test Integer(Fruit.Apple) === Int32(0) +@test Int(Fruit.Apple) === Int(0) +@test Integer(Fruit.Banana) === Int32(1) +@test Int(Fruit.Banana) === Int(1) + +@test Fruit.Apple === Fruit.Apple +@test Fruit.Banana === Fruit.Banana + +@test Fruit.Type(Int32(0)) === Fruit.Type(0) === Fruit.Apple +@test Fruit.Type(Int32(1)) === Fruit.Type(1) === Fruit.Banana +@test_throws ArgumentError("invalid value 123 for Enum Fruit") Fruit.Type(Int32(123)) +@test_throws ArgumentError("invalid value 123 for Enum Fruit") Fruit.Type(123) + +@test Fruit.Apple < Fruit.Banana + +let io = IOBuffer() + write(io, Fruit.Apple) + seekstart(io) + @test read(io, Fruit.Type) === Fruit.Apple + seekstart(io) + write(io, Fruit.Banana) + seekstart(io) + @test read(io, Fruit.Type) === Fruit.Banana + seekstart(io) + write(io, Int32(123)) + seekstart(io) + @test_throws ArgumentError("invalid value 123 for Enum Fruit") read(io, Fruit.Type) +end + +let io = IOBuffer() + show(io, "text/plain", Fruit.Type) + str = String(take!(io)) + @test str == "Enum type Fruit.Type <: Enum{Int32} with 2 instances:\nFruit.Apple = 0\nFruit.Banana = 1" + show(io, "text/plain", Fruit.Apple) + str = String(take!(io)) + @test str == "Fruit.Apple = 0" + show(io, "text/plain", Fruit.Banana) + str = String(take!(io)) + @test str == "Fruit.Banana = 1" +end + end # testset