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:
parent
003fe9f0c8
commit
7b4c9407f2
|
@ -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
|
|
@ -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)
|
|
@ -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.
|
|
@ -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,
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue