From 7b4c9407f29075d3206123c1a2d87fa74af40fd0 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Fri, 26 May 2023 14:41:13 +0200 Subject: [PATCH] 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. --- stew/enums.nim | 59 ++++++++++++++++++++++++++++++ stew/shims/enumutils.nim | 77 +++++++++++++++++++++++++++++++++++++++ stew/shims/typetraits.nim | 15 ++++++++ tests/all_tests.nim | 3 +- tests/test_enums.nim | 37 +++++++++++++++++++ 5 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 stew/enums.nim create mode 100644 stew/shims/enumutils.nim create mode 100644 stew/shims/typetraits.nim create mode 100644 tests/test_enums.nim diff --git a/stew/enums.nim b/stew/enums.nim new file mode 100644 index 0000000..1606a19 --- /dev/null +++ b/stew/enums.nim @@ -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 diff --git a/stew/shims/enumutils.nim b/stew/shims/enumutils.nim new file mode 100644 index 0000000..7750df1 --- /dev/null +++ b/stew/shims/enumutils.nim @@ -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) diff --git a/stew/shims/typetraits.nim b/stew/shims/typetraits.nim new file mode 100644 index 0000000..c56570a --- /dev/null +++ b/stew/shims/typetraits.nim @@ -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. diff --git a/tests/all_tests.nim b/tests/all_tests.nim index 4366d90..a92dd3d 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -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, diff --git a/tests/test_enums.nim b/tests/test_enums.nim new file mode 100644 index 0000000..dc58354 --- /dev/null +++ b/tests/test_enums.nim @@ -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