Skip to content

Commit 7b4c940

Browse files
authored
add enumStyle helper macro (#189)
For serialization and parsing, distinguishing enums with numeric values from enums with associated strings for each value is useful. This adds foundational helpers to allow such distinction.
1 parent 003fe9f commit 7b4c940

File tree

5 files changed

+190
-1
lines changed

5 files changed

+190
-1
lines changed

stew/enums.nim

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# stew
2+
# Copyright 2023 Status Research & Development GmbH
3+
# Licensed under either of
4+
#
5+
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
6+
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
7+
#
8+
# at your option. This file may not be copied, modified, or distributed except according to those terms.
9+
10+
import std/[macros, options]
11+
12+
type EnumStyle* {.pure.} = enum
13+
Numeric,
14+
AssociatedStrings
15+
16+
func setMode(style: var Option[EnumStyle], s: EnumStyle, typ: auto) =
17+
if style.isNone:
18+
style = some s
19+
elif style.get != s:
20+
error("Mixed enum styles not supported for deserialization: " & $typ)
21+
else:
22+
discard
23+
24+
macro enumStyle*(t: typedesc[enum]): untyped =
25+
let
26+
typ = t.getTypeInst[1]
27+
impl = typ.getImpl[2]
28+
expectKind impl, nnkEnumTy
29+
30+
var style: Option[EnumStyle]
31+
for f in impl:
32+
case f.kind
33+
of nnkEmpty:
34+
continue
35+
of nnkIdent:
36+
when (NimMajor, NimMinor) < (1, 4): # `nnkSym` in Nim 1.2
37+
style.setMode(EnumStyle.Numeric, typ)
38+
else:
39+
error("Unexpected enum node for deserialization: " & $f.kind)
40+
of nnkSym:
41+
style.setMode(EnumStyle.Numeric, typ)
42+
of nnkEnumFieldDef:
43+
case f[1].kind
44+
of nnkIntLit:
45+
style.setMode(EnumStyle.Numeric, typ)
46+
of nnkStrLit:
47+
style.setMode(EnumStyle.AssociatedStrings, typ)
48+
else: error("Unexpected enum tuple for deserialization: " & $f[1].kind)
49+
else: error("Unexpected enum node for deserialization: " & $f.kind)
50+
51+
if style.isNone:
52+
error("Cannot determine enum style for deserialization: " & $typ)
53+
case style.get
54+
of EnumStyle.Numeric:
55+
quote do:
56+
EnumStyle.Numeric
57+
of EnumStyle.AssociatedStrings:
58+
quote do:
59+
EnumStyle.AssociatedStrings

stew/shims/enumutils.nim

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
when (NimMajor, NimMinor) > (1, 4):
2+
import std/enumutils
3+
export enumutils
4+
5+
else: # Copy from `std/enumutils`
6+
#
7+
#
8+
# Nim's Runtime Library
9+
# (c) Copyright 2020 Nim contributors
10+
#
11+
# See the file "copying.txt", included in this
12+
# distribution, for details about the copyright.
13+
#
14+
15+
import macros
16+
from typetraits import OrdinalEnum, HoleyEnum
17+
export typetraits
18+
19+
# xxx `genEnumCaseStmt` needs tests and runnableExamples
20+
21+
macro genEnumCaseStmt*(typ: typedesc, argSym: typed, default: typed,
22+
userMin, userMax: static[int], normalizer: static[proc(s :string): string]): untyped =
23+
# generates a case stmt, which assigns the correct enum field given
24+
# a normalized string comparison to the `argSym` input.
25+
# string normalization is done using passed normalizer.
26+
# NOTE: for an enum with fields Foo, Bar, ... we cannot generate
27+
# `of "Foo".nimIdentNormalize: Foo`.
28+
# This will fail, if the enum is not defined at top level (e.g. in a block).
29+
# Thus we check for the field value of the (possible holed enum) and convert
30+
# the integer value to the generic argument `typ`.
31+
let typ = typ.getTypeInst[1]
32+
let impl = typ.getImpl[2]
33+
expectKind impl, nnkEnumTy
34+
let normalizerNode = quote: `normalizer`
35+
expectKind normalizerNode, nnkSym
36+
result = nnkCaseStmt.newTree(newCall(normalizerNode, argSym))
37+
# stores all processed field strings to give error msg for ambiguous enums
38+
var foundFields: seq[string] = @[]
39+
var fStr = "" # string of current field
40+
var fNum = BiggestInt(0) # int value of current field
41+
for f in impl:
42+
case f.kind
43+
of nnkEmpty: continue # skip first node of `enumTy`
44+
of nnkSym, nnkIdent: fStr = f.strVal
45+
of nnkAccQuoted:
46+
fStr = ""
47+
for ch in f:
48+
fStr.add ch.strVal
49+
of nnkEnumFieldDef:
50+
case f[1].kind
51+
of nnkStrLit: fStr = f[1].strVal
52+
of nnkTupleConstr:
53+
fStr = f[1][1].strVal
54+
fNum = f[1][0].intVal
55+
of nnkIntLit:
56+
fStr = f[0].strVal
57+
fNum = f[1].intVal
58+
else: error("Invalid tuple syntax!", f[1])
59+
else: error("Invalid node for enum type `" & $f.kind & "`!", f)
60+
# add field if string not already added
61+
if fNum >= userMin and fNum <= userMax:
62+
fStr = normalizer(fStr)
63+
if fStr notin foundFields:
64+
result.add nnkOfBranch.newTree(newLit fStr, nnkCall.newTree(typ, newLit fNum))
65+
foundFields.add fStr
66+
else:
67+
error("Ambiguous enums cannot be parsed, field " & $fStr &
68+
" appears multiple times!", f)
69+
inc fNum
70+
# finally add else branch to raise or use default
71+
if default == nil:
72+
let raiseStmt = quote do:
73+
raise newException(ValueError, "Invalid enum value: " & $`argSym`)
74+
result.add nnkElse.newTree(raiseStmt)
75+
else:
76+
expectKind(default, nnkSym)
77+
result.add nnkElse.newTree(default)

stew/shims/typetraits.nim

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import std/typetraits
2+
export typetraits
3+
4+
when (NimMajor, NimMinor) < (1, 6): # Copy from `std/typetraits`
5+
#
6+
#
7+
# Nim's Runtime Library
8+
# (c) Copyright 2012 Nim Contributors
9+
#
10+
# See the file "copying.txt", included in this
11+
# distribution, for details about the copyright.
12+
#
13+
14+
type HoleyEnum* = (not Ordinal) and enum ## Enum with holes.
15+
type OrdinalEnum* = Ordinal and enum ## Enum without holes.

tests/all_tests.nim

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# stew
2-
# Copyright 2018-2022 Status Research & Development GmbH
2+
# Copyright 2018-2023 Status Research & Development GmbH
33
# Licensed under either of
44
#
55
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
@@ -21,6 +21,7 @@ import
2121
test_byteutils,
2222
test_ctops,
2323
test_endians2,
24+
test_enums,
2425
test_io2,
2526
test_keyed_queue,
2627
test_sorted_set,

tests/test_enums.nim

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# stew
2+
# Copyright 2023 Status Research & Development GmbH
3+
# Licensed under either of
4+
#
5+
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
6+
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
7+
#
8+
# at your option. This file may not be copied, modified, or distributed except according to those terms.
9+
10+
{.used.}
11+
12+
import
13+
unittest2,
14+
../stew/enums
15+
16+
suite "enumStyle":
17+
test "OrdinalEnum":
18+
type EnumTest = enum
19+
x0,
20+
x1,
21+
x2
22+
check EnumTest.enumStyle == EnumStyle.Numeric
23+
24+
test "HoleyEnum":
25+
type EnumTest = enum
26+
y1 = 1,
27+
y3 = 3,
28+
y4,
29+
y6 = 6
30+
check EnumTest.enumStyle == EnumStyle.Numeric
31+
32+
test "StringEnum":
33+
type EnumTest = enum
34+
z1 = "aaa",
35+
z2 = "bbb",
36+
z3 = "ccc"
37+
check EnumTest.enumStyle == EnumStyle.AssociatedStrings

0 commit comments

Comments
 (0)