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.
This commit is contained in:
Etan Kissling 2023-05-26 14:41:13 +02:00 committed by GitHub
parent 003fe9f0c8
commit 7b4c9407f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 190 additions and 1 deletions

59
stew/enums.nim Normal file
View File

@ -0,0 +1,59 @@
# stew
# Copyright 2023 Status Research & Development GmbH
# Licensed under either of
#
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
#
# at your option. This file may not be copied, modified, or distributed except according to those terms.
import std/[macros, options]
type EnumStyle* {.pure.} = enum
Numeric,
AssociatedStrings
func setMode(style: var Option[EnumStyle], s: EnumStyle, typ: auto) =
if style.isNone:
style = some s
elif style.get != s:
error("Mixed enum styles not supported for deserialization: " & $typ)
else:
discard
macro enumStyle*(t: typedesc[enum]): untyped =
let
typ = t.getTypeInst[1]
impl = typ.getImpl[2]
expectKind impl, nnkEnumTy
var style: Option[EnumStyle]
for f in impl:
case f.kind
of nnkEmpty:
continue
of nnkIdent:
when (NimMajor, NimMinor) < (1, 4): # `nnkSym` in Nim 1.2
style.setMode(EnumStyle.Numeric, typ)
else:
error("Unexpected enum node for deserialization: " & $f.kind)
of nnkSym:
style.setMode(EnumStyle.Numeric, typ)
of nnkEnumFieldDef:
case f[1].kind
of nnkIntLit:
style.setMode(EnumStyle.Numeric, typ)
of nnkStrLit:
style.setMode(EnumStyle.AssociatedStrings, typ)
else: error("Unexpected enum tuple for deserialization: " & $f[1].kind)
else: error("Unexpected enum node for deserialization: " & $f.kind)
if style.isNone:
error("Cannot determine enum style for deserialization: " & $typ)
case style.get
of EnumStyle.Numeric:
quote do:
EnumStyle.Numeric
of EnumStyle.AssociatedStrings:
quote do:
EnumStyle.AssociatedStrings

77
stew/shims/enumutils.nim Normal file
View File

@ -0,0 +1,77 @@
when (NimMajor, NimMinor) > (1, 4):
import std/enumutils
export enumutils
else: # Copy from `std/enumutils`
#
#
# Nim's Runtime Library
# (c) Copyright 2020 Nim contributors
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
import macros
from typetraits import OrdinalEnum, HoleyEnum
export typetraits
# xxx `genEnumCaseStmt` needs tests and runnableExamples
macro genEnumCaseStmt*(typ: typedesc, argSym: typed, default: typed,
userMin, userMax: static[int], normalizer: static[proc(s :string): string]): untyped =
# generates a case stmt, which assigns the correct enum field given
# a normalized string comparison to the `argSym` input.
# string normalization is done using passed normalizer.
# NOTE: for an enum with fields Foo, Bar, ... we cannot generate
# `of "Foo".nimIdentNormalize: Foo`.
# This will fail, if the enum is not defined at top level (e.g. in a block).
# Thus we check for the field value of the (possible holed enum) and convert
# the integer value to the generic argument `typ`.
let typ = typ.getTypeInst[1]
let impl = typ.getImpl[2]
expectKind impl, nnkEnumTy
let normalizerNode = quote: `normalizer`
expectKind normalizerNode, nnkSym
result = nnkCaseStmt.newTree(newCall(normalizerNode, argSym))
# stores all processed field strings to give error msg for ambiguous enums
var foundFields: seq[string] = @[]
var fStr = "" # string of current field
var fNum = BiggestInt(0) # int value of current field
for f in impl:
case f.kind
of nnkEmpty: continue # skip first node of `enumTy`
of nnkSym, nnkIdent: fStr = f.strVal
of nnkAccQuoted:
fStr = ""
for ch in f:
fStr.add ch.strVal
of nnkEnumFieldDef:
case f[1].kind
of nnkStrLit: fStr = f[1].strVal
of nnkTupleConstr:
fStr = f[1][1].strVal
fNum = f[1][0].intVal
of nnkIntLit:
fStr = f[0].strVal
fNum = f[1].intVal
else: error("Invalid tuple syntax!", f[1])
else: error("Invalid node for enum type `" & $f.kind & "`!", f)
# add field if string not already added
if fNum >= userMin and fNum <= userMax:
fStr = normalizer(fStr)
if fStr notin foundFields:
result.add nnkOfBranch.newTree(newLit fStr, nnkCall.newTree(typ, newLit fNum))
foundFields.add fStr
else:
error("Ambiguous enums cannot be parsed, field " & $fStr &
" appears multiple times!", f)
inc fNum
# finally add else branch to raise or use default
if default == nil:
let raiseStmt = quote do:
raise newException(ValueError, "Invalid enum value: " & $`argSym`)
result.add nnkElse.newTree(raiseStmt)
else:
expectKind(default, nnkSym)
result.add nnkElse.newTree(default)

15
stew/shims/typetraits.nim Normal file
View File

@ -0,0 +1,15 @@
import std/typetraits
export typetraits
when (NimMajor, NimMinor) < (1, 6): # Copy from `std/typetraits`
#
#
# Nim's Runtime Library
# (c) Copyright 2012 Nim Contributors
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
type HoleyEnum* = (not Ordinal) and enum ## Enum with holes.
type OrdinalEnum* = Ordinal and enum ## Enum without holes.

View File

@ -1,5 +1,5 @@
# stew
# Copyright 2018-2022 Status Research & Development GmbH
# Copyright 2018-2023 Status Research & Development GmbH
# Licensed under either of
#
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
@ -21,6 +21,7 @@ import
test_byteutils,
test_ctops,
test_endians2,
test_enums,
test_io2,
test_keyed_queue,
test_sorted_set,

37
tests/test_enums.nim Normal file
View File

@ -0,0 +1,37 @@
# stew
# Copyright 2023 Status Research & Development GmbH
# Licensed under either of
#
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
#
# at your option. This file may not be copied, modified, or distributed except according to those terms.
{.used.}
import
unittest2,
../stew/enums
suite "enumStyle":
test "OrdinalEnum":
type EnumTest = enum
x0,
x1,
x2
check EnumTest.enumStyle == EnumStyle.Numeric
test "HoleyEnum":
type EnumTest = enum
y1 = 1,
y3 = 3,
y4,
y6 = 6
check EnumTest.enumStyle == EnumStyle.Numeric
test "StringEnum":
type EnumTest = enum
z1 = "aaa",
z2 = "bbb",
z3 = "ccc"
check EnumTest.enumStyle == EnumStyle.AssociatedStrings