Skip to content

Commit 7aa0ec2

Browse files
authored
Merge pull request #2 from jw3126/enum
add @enumbatteries
2 parents cc33132 + d690c6a commit 7aa0ec2

File tree

4 files changed

+205
-7
lines changed

4 files changed

+205
-7
lines changed

.github/workflows/CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ jobs:
1010
fail-fast: false
1111
matrix:
1212
version:
13-
- '1.3'
1413
- '1.6'
14+
- '1'
1515
- 'nightly'
1616
os:
1717
- ubuntu-latest

Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
name = "StructHelpers"
22
uuid = "4093c41a-2008-41fd-82b8-e3f9d02b504f"
33
authors = ["Jan Weidner <[email protected]> and contributors"]
4-
version = "0.1.3"
4+
version = "0.1.4"
55

66
[deps]
77
ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9"
88

99
[compat]
10-
julia = "1.3"
10+
julia = "1.6"
1111
ConstructionBase = "1.3"
1212

1313
[extras]

src/StructHelpers.jl

Lines changed: 161 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module StructHelpers
22

33
export @batteries
4+
export @enumbatteries
45

56
import ConstructionBase: getproperties, constructorof, setproperties
67

@@ -72,8 +73,7 @@ function doc_batteries_options()
7273
join(lines, "\n")
7374
end
7475

75-
76-
const ALLOWED_KW = keys(BATTERIES_DEFAULTS)
76+
const BATTERIES_ALLOWED_KW = keys(BATTERIES_DEFAULTS)
7777

7878
"""
7979
@@ -104,7 +104,7 @@ macro batteries(T, kw...)
104104
error("""
105105
Unsupported keyword.
106106
Offending Keyword: $pname
107-
allowed: $ALLOWED_KW
107+
allowed: $BATTERIES_ALLOWED_KW
108108
Got: $nt
109109
""")
110110
end
@@ -193,4 +193,162 @@ function parse_all_macro_kw(kw)
193193
(;pairs...)
194194
end
195195

196+
################################################################################
197+
#### enum
198+
################################################################################
199+
function ifelsechain(
200+
cond_code_pairs,
201+
rest
202+
)
203+
if length(cond_code_pairs) == 0
204+
return rest
205+
elseif length(cond_code_pairs) == 1
206+
cond, code = only(cond_code_pairs)
207+
Expr(:if, cond, code, rest)
208+
else
209+
cond, code = cond_code_pairs[end]
210+
ifelsechain(
211+
cond_code_pairs[begin:end-1],
212+
Expr(:elseif, cond, code, rest),
213+
)
214+
end
215+
end
216+
217+
function enum_from_string end
218+
function enum_from_symbol end
219+
function string_from_enum(x)::String
220+
string(x)
221+
end
222+
function symbol_from_enum(x)::Symbol
223+
Symbol(string_from_enum(x))
224+
end
225+
226+
function def_enum_from_string(T)::Expr
227+
body = def_symbol_or_enum_from_string_body(string_from_enum, T)
228+
:(
229+
function StructHelpers.enum_from_string(::Type{$T}, s::String)::$T
230+
$body
231+
end
232+
)
233+
end
234+
function def_enum_from_symbol(T)::Expr
235+
body = def_symbol_or_enum_from_string_body(QuoteNodesymbol_from_enum, T)
236+
:(
237+
function StructHelpers.enum_from_symbol(::Type{$T}, s::Symbol)::$T
238+
$body
239+
end
240+
)
241+
end
242+
243+
@noinline function throw_no_matching_instance(f,T,s)
244+
msg = """
245+
Cannot instaniate enum `T` from `s`. Got:
246+
s = $(repr(s))
247+
T = $(T)
248+
allowed values for s = $(map(f, instances(T)))
249+
"""
250+
throw(ArgumentError(msg))
251+
end
252+
253+
function def_symbol_or_enum_from_string_body(f,T)
254+
err = :($throw_no_matching_instance($f,$T,s))
255+
matcharms = [
256+
:(s === $(f(inst))) => inst for inst in instances(T)
257+
]
258+
ifelsechain(matcharms, err)
259+
end
260+
261+
const ENUM_BATTERIES_DEFAULTS = (
262+
string_conversion=false,
263+
symbol_conversion=false,
264+
)
265+
266+
const ENUM_BATTERIES_DOCSTRINGS = (
267+
string_conversion="Add `convert(MyEnum, ::String)`, `MyEnum(::String)`, `convert(String, ::MyEnum)` and `String(::MyEnum)`",
268+
symbol_conversion="Add `convert(MyEnum, ::Symbol)`, `MyEnum(::Symbol)`, `convert(Symbol, ::MyEnum)` and `Symbol(::MyEnum)`",
269+
)
270+
271+
if (keys(ENUM_BATTERIES_DEFAULTS) != keys(ENUM_BATTERIES_DOCSTRINGS))
272+
error("""
273+
keys(ENUM_BATTERIES_DEFAULTS) == key(ENUM_BATTERIES_DOCSTRINGS) must hold.
274+
Got:
275+
keys(ENUM_BATTERIES_DEFAULTS) = $(keys(ENUM_BATTERIES_DEFAULTS))
276+
keys(ENUM_BATTERIES_DOCSTRINGS) = $(keys(ENUM_BATTERIES_DOCSTRINGS))
277+
""")
196278
end
279+
@assert keys(ENUM_BATTERIES_DEFAULTS) == keys(ENUM_BATTERIES_DOCSTRINGS)
280+
281+
function doc_enum_batteries_options()
282+
lines = map(propertynames(ENUM_BATTERIES_DEFAULTS)) do key
283+
"* **$key** = $(ENUM_BATTERIES_DEFAULTS[key]):\n $(ENUM_BATTERIES_DOCSTRINGS[key])"
284+
end
285+
join(lines, "\n")
286+
end
287+
288+
const ENUM_BATTERIES_ALLOWED_KW = keys(ENUM_BATTERIES_DEFAULTS)
289+
290+
"""
291+
292+
@enumbatteries T [options]
293+
294+
Automatically derive several methods for Enum type `T`.
295+
296+
# Example
297+
```julia
298+
@enum Color Red Blue Yellow
299+
@enumbatteries Color
300+
@enumbatteries Color hash=false # don't overload `Base.hash`
301+
@enumbatteries Color symbol_conversion=true # allow convert(Color, :Blue), Color(:Blue), convert(Symbol, Blue), Symbol(Blue)
302+
```
303+
304+
Supported options and defaults are:
305+
306+
$(doc_enum_batteries_options())
307+
"""
308+
macro enumbatteries(T, kw...)
309+
nt = parse_all_macro_kw(kw)
310+
for (pname, val) in pairs(nt)
311+
if !(pname in propertynames(ENUM_BATTERIES_DEFAULTS))
312+
error("""
313+
Unsupported keyword.
314+
Offending Keyword: $pname
315+
allowed: $ENUM_BATTERIES_ALLOWED_KW
316+
Got: $nt
317+
""")
318+
end
319+
if val isa Bool
320+
321+
else
322+
error("""
323+
Bad keyword argument value:
324+
Got: $nt
325+
Offending Keyword: $pname
326+
Offending value : $(repr(val))
327+
""")
328+
end
329+
end
330+
nt = merge(ENUM_BATTERIES_DEFAULTS, nt)
331+
TT = Base.eval(__module__, T)::Type
332+
ret = quote end
333+
334+
push!(ret.args, :(import StructHelpers))
335+
push!(ret.args, def_enum_from_symbol(TT))
336+
push!(ret.args, def_enum_from_string(TT))
337+
if nt.string_conversion
338+
ex1 = :(Base.convert(::Type{$TT}, s::AbstractString) = StructHelpers.enum_from_string($TT, String(s)))
339+
ex2 = :($T(s::AbstractString) = StructHelpers.enum_from_string($TT, String(s)))
340+
ex3 = :(Base.convert(::Type{String}, x::$T) = StructHelpers.string_from_enum(x))
341+
ex4 = :(Base.String(x::$T) = StructHelpers.string_from_enum(x))
342+
push!(ret.args, ex1, ex2, ex3, ex4)
343+
end
344+
if nt.symbol_conversion
345+
ex1 = :(Base.convert(::Type{$T}, s::Symbol) = StructHelpers.enum_from_symbol($TT, Symbol(s)))
346+
ex2 = :($T(s::Symbol) = StructHelpers.enum_from_symbol($TT, Symbol(s)))
347+
ex3 = :(Base.convert(::Type{Symbol}, x::$T) = StructHelpers.symbol_from_enum(x))
348+
ex4 = :(Base.Symbol(x::$T) = StructHelpers.symbol_from_enum(x))
349+
push!(ret.args, ex1, ex2, ex3, ex4)
350+
end
351+
return esc(ret)
352+
end
353+
354+
end #module

test/runtests.jl

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using StructHelpers: @batteries, StructHelpers
1+
using StructHelpers: @batteries, StructHelpers, @enumbatteries
22
const SH = StructHelpers
33
using Test
44

@@ -81,3 +81,43 @@ struct SErrors;a;b;c;end
8181
@test hash(Salt1()) != hash(NoSalt())
8282
@test hash(Salt1()) != hash(Salt2())
8383
end
84+
85+
@enum Color Red Blue Green
86+
87+
@enumbatteries Color string_conversion = true symbol_conversion = true
88+
89+
@enum Shape Circle Square
90+
@enumbatteries Shape symbol_conversion =true
91+
92+
@testset "@enumbatteries" begin
93+
@test Red === @inferred Color("Red")
94+
@test Red === @inferred convert(Color, "Red")
95+
@test "Red" === @inferred String(Red)
96+
@test "Red" === @inferred convert(String, Red)
97+
@test_throws ArgumentError Color("Nonsense")
98+
99+
@test :Red === @inferred Symbol(Red)
100+
@test :Red === @inferred convert(Symbol, Red)
101+
@test Red === @inferred Color(:Red)
102+
@test Red === @inferred convert(Color, :Red)
103+
@test_throws ArgumentError Color(:Nonsense)
104+
res = @test_throws ArgumentError convert(Color, :nonsense)
105+
@test occursin(":nonsense", res.value.msg)
106+
@test occursin(":Red", res.value.msg)
107+
@test occursin(":Blue", res.value.msg)
108+
@test occursin(":Green", res.value.msg)
109+
110+
@test :Circle === @inferred Symbol(Circle)
111+
@test :Circle === @inferred convert(Symbol, Circle)
112+
@test Circle === @inferred Shape(:Circle)
113+
@test Circle === @inferred convert(Shape, :Circle)
114+
@test_throws ArgumentError Shape(:Nonsense)
115+
res = @test_throws ArgumentError convert(Shape, :nonsense)
116+
@test occursin(":Circle", res.value.msg)
117+
@test occursin(":Square", res.value.msg)
118+
119+
@test_throws Exception String(Circle)
120+
@test_throws Exception convert(String, Circle)
121+
@test_throws Exception Shape("Circle")
122+
@test_throws Exception convert(Shape, "Circle")
123+
end

0 commit comments

Comments
 (0)