diff options
author | Vindaar <basti90@gmail.com> | 2020-04-22 10:41:56 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-04-22 09:41:56 +0100 |
commit | d42c5a575dfaa6df363fd47c95d13c70aa60729c (patch) | |
tree | a8762ad78d20a9a5c1891babd33fb6b5ffb497f6 | |
parent | c3f4b93060b5e72b5ee1a4e37d5c69e63e7d8bb8 (diff) | |
download | Nim-d42c5a575dfaa6df363fd47c95d13c70aa60729c.tar.gz |
base `parseEnum` on a case statement, fixes #14030 (#14046)
* base `parseEnum` on a case statement, fixes #14030 * apply simplifactions / clean up, remove `norm` node, use strVal * export `normalize` in json.nim * cmp using nimIdentNormalize, error at CT if ambiguous enum found `nimIdentNormalize` provided by @cooldome. We track all names of the branches we have created so far and error if a duplicate is found. Dummy change to make github react... * fix docstring of `nimIdentNormalize` * make `typ` arg `typedesc`, add lineinfo, call norm. only once
-rw-r--r-- | lib/pure/json.nim | 2 | ||||
-rw-r--r-- | lib/pure/strutils.nim | 93 | ||||
-rw-r--r-- | tests/stdlib/tstrutil.nim | 87 |
3 files changed, 171 insertions, 11 deletions
diff --git a/lib/pure/json.nim b/lib/pure/json.nim index 7921c0c05..20608edef 100644 --- a/lib/pure/json.nim +++ b/lib/pure/json.nim @@ -158,7 +158,7 @@ export export parsejson.JsonEventKind, parsejson.JsonError, JsonParser, JsonKindError, open, close, str, getInt, getFloat, kind, getColumn, getLine, getFilename, - errorMsg, errorMsgExpected, next, JsonParsingError, raiseParseErr + errorMsg, errorMsgExpected, next, JsonParsingError, raiseParseErr, nimIdentNormalize type JsonNodeKind* = enum ## possible JSON node types diff --git a/lib/pure/strutils.nim b/lib/pure/strutils.nim index 535af2b31..ff76fa94f 100644 --- a/lib/pure/strutils.nim +++ b/lib/pure/strutils.nim @@ -75,6 +75,7 @@ import parseutils from math import pow, floor, log10 from algorithm import reverse +import macros # for `parseEnum` when defined(nimVmExportFixed): from unicode import toLower, toUpper @@ -278,6 +279,26 @@ proc capitalizeAscii*(s: string): string {.noSideEffect, procvar, if s.len == 0: result = "" else: result = toUpperAscii(s[0]) & substr(s, 1) +proc nimIdentNormalize*(s: string): string = + ## Normalizes the string `s` as a Nim identifier. + ## + ## That means to convert to lower case and remove any '_' on all characters + ## except first one. + runnableExamples: + doAssert nimIdentNormalize("Foo_bar") == "Foobar" + result = newString(s.len) + if s.len > 0: + result[0] = s[0] + var j = 1 + for i in 1..len(s) - 1: + if s[i] in {'A'..'Z'}: + result[j] = chr(ord(s[i]) + (ord('a') - ord('A'))) + inc j + elif s[i] != '_': + result[j] = s[i] + inc j + if j != s.len: setLen(result, j) + proc normalize*(s: string): string {.noSideEffect, procvar, rtl, extern: "nsuNormalize".} = ## Normalizes the string `s`. @@ -1229,8 +1250,65 @@ proc parseBool*(s: string): bool = of "n", "no", "false", "0", "off": result = false else: raise newException(ValueError, "cannot interpret as a bool: " & s) +proc addOfBranch(s: string, field, enumType: NimNode): NimNode = + result = nnkOfBranch.newTree( + newLit s, + nnkCall.newTree(enumType, field) # `T(<fieldValue>)` + ) + +macro genEnumStmt(typ: typedesc, argSym: typed, default: typed): untyped = + # generates a case stmt, which assigns the correct enum field given + # a normalized string comparison to the `argSym` input. + # 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 + result = nnkCaseStmt.newTree(nnkDotExpr.newTree(argSym, + bindSym"nimIdentNormalize")) + # stores all processed field strings to give error msg for ambiguous enums + var foundFields: seq[string] + var fStr: string # string of current field + var fNum: BiggestInt # 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 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) + # add field if string not already added + fStr = nimIdentNormalize(fStr) + if fStr notin foundFields: + result.add addOfBranch(fStr, newLit fNum, typ) + foundFields.add fStr + else: + error("Ambiguous enums cannot be parsed, field " & $fStr & + " appears multiple times!") + 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) + proc parseEnum*[T: enum](s: string): T = - ## Parses an enum ``T``. + ## Parses an enum ``T``. This errors at compile time, if the given enum + ## type contains multiple fields with the same string value. ## ## Raises ``ValueError`` for an invalid value in `s`. The comparison is ## done in a style insensitive way. @@ -1246,13 +1324,11 @@ proc parseEnum*[T: enum](s: string): T = doAssertRaises(ValueError): echo parseEnum[MyEnum]("third") - for e in low(T)..high(T): - if cmpIgnoreStyle(s, $e) == 0: - return e - raise newException(ValueError, "invalid enum value: " & s) + genEnumStmt(T, s, default = nil) proc parseEnum*[T: enum](s: string, default: T): T = - ## Parses an enum ``T``. + ## Parses an enum ``T``. This errors at compile time, if the given enum + ## type contains multiple fields with the same string value. ## ## Uses `default` for an invalid value in `s`. The comparison is done in a ## style insensitive way. @@ -1267,10 +1343,7 @@ proc parseEnum*[T: enum](s: string, default: T): T = doAssert parseEnum[MyEnum]("second") == second doAssert parseEnum[MyEnum]("last", third) == third - for e in low(T)..high(T): - if cmpIgnoreStyle(s, $e) == 0: - return e - result = default + genEnumStmt(T, s, default) proc repeat*(c: char, count: Natural): string {.noSideEffect, rtl, extern: "nsuRepeatChar".} = diff --git a/tests/stdlib/tstrutil.nim b/tests/stdlib/tstrutil.nim index 9dbc98912..eb0fdbfa5 100644 --- a/tests/stdlib/tstrutil.nim +++ b/tests/stdlib/tstrutil.nim @@ -348,3 +348,90 @@ when true: main() #OUT ha/home/a1xyz/usr/bin + + +# `parseEnum`, ref issue #14030 +# check enum defined at top level +type + Foo = enum + A + B = "bb" + C = (5, "ccc") + D = 15 + E = "ee" # check that we count enum fields correctly + +block: + let a = parseEnum[Foo]("A") + let b = parseEnum[Foo]("bb") + let c = parseEnum[Foo]("ccc") + let d = parseEnum[Foo]("D") + let e = parseEnum[Foo]("ee") + doAssert a == A + doAssert b == B + doAssert c == C + doAssert d == D + doAssert e == E + try: + let f = parseEnum[Foo]("Bar") + doAssert false + except ValueError: + discard + + # finally using default + let g = parseEnum[Foo]("Bar", A) + doAssert g == A + +block: + # check enum defined in block + type + Bar = enum + V + W = "ww" + X = (3, "xx") + Y = 10 + Z = "zz" # check that we count enum fields correctly + + let a = parseEnum[Bar]("V") + let b = parseEnum[Bar]("ww") + let c = parseEnum[Bar]("xx") + let d = parseEnum[Bar]("Y") + let e = parseEnum[Bar]("zz") + doAssert a == V + doAssert b == W + doAssert c == X + doAssert d == Y + doAssert e == Z + try: + let f = parseEnum[Bar]("Baz") + doAssert false + except ValueError: + discard + + # finally using default + let g = parseEnum[Bar]("Baz", V) + doAssert g == V + +block: + # check ambiguous enum fails to parse + type + Ambig = enum + f1 = "A" + f2 = "B" + f3 = "A" + + doAssert not compiles((let a = parseEnum[Ambig]("A"))) + +block: + # check almost ambiguous enum + type + AlmostAmbig = enum + f1 = "someA" + f2 = "someB" + f3 = "SomeA" + + let a = parseEnum[AlmostAmbig]("someA") + let b = parseEnum[AlmostAmbig]("someB") + let c = parseEnum[AlmostAmbig]("SomeA") + doAssert a == f1 + doAssert b == f2 + doAssert c == f3 |