Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "StructHelpers"
uuid = "4093c41a-2008-41fd-82b8-e3f9d02b504f"
authors = ["Jan Weidner <jw3126@gmail.com> and contributors"]
version = "1.2.0"
version = "1.3.0"

[deps]
ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9"
Expand Down
66 changes: 43 additions & 23 deletions src/StructHelpers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ of `obj` or handle them in a special way.
"""
function hash_eq_as(obj)
# it would be better to just use getproperties
# but this would cause hashed to change, which we want to
# but this would cause hashed to change, which we want to
# keep backwards compatible for now.
#
# TODO: Change to getproperties once we want to make a hash breaking change
Expand All @@ -50,10 +50,10 @@ end
isequal(getproperties(o1), getproperties(o2))
end

function start_hash(o, h, typesalt::Nothing)
function start_hash(o, h, typesalt::Nothing)
Base.hash(typeof(o), h)
end
function start_hash(o, h, typesalt)
function start_hash(o, h, typesalt)
Base.hash(typesalt, h)
end

Expand Down Expand Up @@ -166,15 +166,10 @@ macro batteries(T, kw...)
Got: $nt
""")
end
if val isa Bool

elseif pname == :typesalt
typesalt = val
if !(typesalt isa Union{Nothing,Integer})
error("""`typesalt` must be literally `nothing` or an unsigned integer. Got:
typesalt = $(repr(typesalt))::$(typeof(typesalt))
""")
end
if pname == :typesalt
check_typesalt(val)
elseif val isa Bool
# pass
else
error("""
Bad keyword argument value:
Expand All @@ -197,7 +192,7 @@ macro batteries(T, kw...)
push!(ret.args, :(import StructTypes as $ST))
end
if nt.hash
def = :(function Base.hash(o::$T, h::UInt)
def = :(function Base.hash(o::$T, h::UInt)
h = ($start_hash)(o, h, $(nt.typesalt))
proxy = ($hash_eq_as)(o)
Base.hash(proxy, h)
Expand All @@ -213,7 +208,7 @@ macro batteries(T, kw...)
push!(ret.args, def)
end
if nt.isequal
def = :(function Base.isequal(o1::$T, o2::$T)
def = :(function Base.isequal(o1::$T, o2::$T)
isequal($hash_eq_as(o1), $hash_eq_as(o2))
end
)
Expand Down Expand Up @@ -255,6 +250,17 @@ function def_has_batteries(T)
)
end

function check_typesalt(typesalt)
if !(typesalt isa Union{Nothing,Integer})
error(
"""
`typesalt` must be literally `nothing` or an unsigned integer. Got:
typesalt = $(repr(typesalt))::$(typeof(typesalt))
"""
)
end
end

function error_parse_macro_kw(kw; comment=nothing)
msg = """
Expected a keyword argument of the form name = value.
Expand Down Expand Up @@ -351,15 +357,19 @@ function def_symbol_or_enum_from_string_body(f,T)
end

const ENUM_BATTERIES_DEFAULTS = (
string_conversion=false,
symbol_conversion=false,
selfconstructor=BATTERIES_DEFAULTS.selfconstructor,
string_conversion = false,
symbol_conversion = false,
selfconstructor = BATTERIES_DEFAULTS.selfconstructor,
hash = false,
typesalt = BATTERIES_DEFAULTS.typesalt,
)

const ENUM_BATTERIES_DOCSTRINGS = (
string_conversion="Add `convert(MyEnum, ::String)`, `MyEnum(::String)`, `convert(String, ::MyEnum)` and `String(::MyEnum)`",
symbol_conversion="Add `convert(MyEnum, ::Symbol)`, `MyEnum(::Symbol)`, `convert(Symbol, ::MyEnum)` and `Symbol(::MyEnum)`",
selfconstructor=BATTERIES_DOCSTRINGS.selfconstructor,
string_conversion = "Add `convert(MyEnum, ::String)`, `MyEnum(::String)`, `convert(String, ::MyEnum)` and `String(::MyEnum)`",
symbol_conversion = "Add `convert(MyEnum, ::Symbol)`, `MyEnum(::Symbol)`, `convert(Symbol, ::MyEnum)` and `Symbol(::MyEnum)`",
selfconstructor = BATTERIES_DOCSTRINGS.selfconstructor,
hash = BATTERIES_DOCSTRINGS.hash,
typesalt = BATTERIES_DOCSTRINGS.typesalt,
)

if (keys(ENUM_BATTERIES_DEFAULTS) != keys(ENUM_BATTERIES_DOCSTRINGS))
Expand Down Expand Up @@ -410,8 +420,10 @@ macro enumbatteries(T, kw...)
Got: $nt
""")
end
if val isa Bool

if pname == :typesalt
check_typesalt(val)
elseif val isa Bool
# pass
else
error("""
Bad keyword argument value:
Expand All @@ -421,7 +433,7 @@ macro enumbatteries(T, kw...)
""")
end
end
nt = merge(ENUM_BATTERIES_DEFAULTS, nt)
nt = merge(ENUM_BATTERIES_DEFAULTS, (; hash = haskey(nt, :typesalt)), nt)
TT = Base.eval(__module__, T)::Type
ret = quote end

Expand All @@ -446,6 +458,14 @@ macro enumbatteries(T, kw...)
def = def_selfconstructor(T)
push!(ret.args, def)
end
if nt.hash
def = :(function Base.hash(o::$T, h::UInt)
h = ($start_hash)(o, h, $(nt.typesalt))
Base.hash(UInt(o), h)
end
)
push!(ret.args, def)
end
push!(ret.args, def_has_batteries(T))
return esc(ret)
end
Expand Down
31 changes: 24 additions & 7 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ struct SNoIsEqual; a; end
@test Empty1() !== Empty2()
@test Empty1() != Empty2()
@test hash(Empty1()) != hash(Empty2())

if VERSION >= v"1.8"
@test_throws "Bad keyword argument value:" @macroexpand @batteries SErrors kwconstructor="true"
@test_throws "Unsupported keyword" @macroexpand @batteries SErrors kwconstructor=true nonsense=true
Expand All @@ -102,7 +102,7 @@ struct SNoIsEqual; a; end
@test_throws Exception @macroexpand @batteries SErrors kwconstructor=true nonsense=true
@test_throws Exception @macroexpand @batteries SErrors nonsense
end


@testset "typesalt" begin
@test hash(Salt1()) === hash(Salt1b())
Expand Down Expand Up @@ -131,8 +131,11 @@ end
@enum Color Red Blue Green
@enumbatteries Color string_conversion = true symbol_conversion = true selfconstructor = false

@enum Shape Circle Square
@enumbatteries Shape symbol_conversion =true
@enum Shape Circle = 7 Square = 8
@enumbatteries Shape symbol_conversion = true typesalt = 0x0578044908fb9846

@enum Size Small Medium Large
@enumbatteries Size hash = true

@testset "@enumbatteries" begin
@test SH.has_batteries(Color)
Expand Down Expand Up @@ -171,13 +174,27 @@ end
@test_throws Exception convert(Shape, "Circle")
end

@testset "@enumbatteries hash" begin
# hash with typesalt
@test hash(Circle) == hash(7, hash(0x0578044908fb9846))
@test hash(Square) == hash(8, hash(0x0578044908fb9846))

# hash = true
@test hash(Small) == hash(0, hash(Size))
@test hash(Medium) == hash(1, hash(Size))
@test hash(Large) == hash(2, hash(Size))

# no hash by default
@test hash(Red) != hash(0, hash(Color))
end

struct Bad end
@testset "Error messages" begin
@macroexpand @batteries Bad
@macroexpand @batteries Bad typesalt = 0xb6a4b9eeeb03b58b
if VERSION >= v"1.7"
@test_throws "`typesalt` must be literally `nothing` or an unsigned integer." @macroexpand @batteries Bad typesalt = "ouch"
@test_throws "Unsupported keyword." @macroexpand @batteries Bad does_not_exist = true
@test_throws "Unsupported keyword." @macroexpand @batteries Bad does_not_exist = true
@test_throws "Bad keyword argument value" @macroexpand @batteries Bad hash=:nonsense
@test_throws "Bad keyword argument value" @macroexpand @batteries Bad StructTypes=:nonsense
end
Expand All @@ -192,7 +209,7 @@ struct HashEqAs <: AbstractHashEqAs
hash_eq_as
payload
end
SH.@batteries HashEqAs
SH.@batteries HashEqAs
struct HashEqAsTS1 <: AbstractHashEqAs
hash_eq_as
payload
Expand Down Expand Up @@ -250,7 +267,7 @@ Base.:(==)(::HashEqErr, ::HashEqErr) = error()
@test SH.structural_hash(S(2,NaN), UInt(0)) != SH.structural_hash(S(2,NaN), UInt(1))
end

struct WithStructTypes
struct WithStructTypes
x
y
end
Expand Down
Loading