Add compile time switch to alter encoder enum representation (#95)
* Add compile time switch to alter encoder enum representation * Fix test * Add enum section in README.md Add more tests and encoder features of enums * Add enums section to README.md
This commit is contained in:
parent
6ff807654f
commit
8a4ed98bbd
59
README.md
59
README.md
|
@ -261,6 +261,65 @@ writeNumber[F,T](w: var JsonWriter[F], value: JsonNumber[T])
|
|||
writeJsonValueRef[F,T](w: var JsonWriter[F], value: JsonValueRef[T])
|
||||
```
|
||||
|
||||
## Enums
|
||||
|
||||
```Nim
|
||||
type
|
||||
Fruit = enum
|
||||
Apple = "Apple"
|
||||
Banana = "Banana"
|
||||
|
||||
Drawer = enum
|
||||
One
|
||||
Two
|
||||
|
||||
Number = enum
|
||||
Three = 3
|
||||
Four = 4
|
||||
|
||||
Mixed = enum
|
||||
Six = 6
|
||||
Seven = "Seven"
|
||||
```
|
||||
|
||||
nim-json-serialization automatically detect which representation an enum should be parsed.
|
||||
The detection occurs when parse json literal and from the enum declaration itself.
|
||||
'Fruit' expect string literal. 'Drawer' or 'Number' expect numeric literal.
|
||||
'Mixed' is disallowed. If the json literal does not match the expected enum style,
|
||||
exception will be raised. But you can configure individual enum type with:
|
||||
|
||||
```Nim
|
||||
configureJsonDeserialization(
|
||||
T: type[enum], allowNumericRepr: static[bool] = false,
|
||||
stringNormalizer: static[proc(s: string): string] = strictNormalize)
|
||||
|
||||
# example:
|
||||
Mixed.configureJsonDeserialization(allowNumericRepr = true) # only at top level
|
||||
```
|
||||
|
||||
When encode an enum, user is also given flexibility to configure at Flavor level
|
||||
or for individual enum type.
|
||||
|
||||
```Nim
|
||||
type
|
||||
EnumRepresentation* = enum
|
||||
EnumAsString
|
||||
EnumAsNumber
|
||||
EnumAsStringifiedNumber
|
||||
|
||||
# examples:
|
||||
|
||||
# Flavor level
|
||||
Json.flavorEnumRep(EnumAsString) # default flavor, can be called from non top level
|
||||
Flavor.flavorEnumRep(EnumAsNumber) # custom flavor, can be called from non top level
|
||||
|
||||
# individual enum type no matter what flavor
|
||||
Fruit.configureJsonSerialization(EnumAsNumber) # only at top level
|
||||
|
||||
# individual enum type of specific flavor
|
||||
MyJson.flavorEnumRep(Drawer, EnumAsString) # only at top level
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Licensed and distributed under either of
|
||||
|
|
|
@ -20,12 +20,31 @@ template supports*(_: type Json, T: type): bool =
|
|||
# The JSON format should support every type
|
||||
true
|
||||
|
||||
type
|
||||
EnumRepresentation* = enum
|
||||
EnumAsString
|
||||
EnumAsNumber
|
||||
EnumAsStringifiedNumber
|
||||
|
||||
template flavorUsesAutomaticObjectSerialization*(T: type DefaultFlavor): bool = true
|
||||
template flavorOmitsOptionalFields*(T: type DefaultFlavor): bool = true
|
||||
template flavorRequiresAllFields*(T: type DefaultFlavor): bool = false
|
||||
template flavorAllowsUnknownFields*(T: type DefaultFlavor): bool = false
|
||||
template flavorSkipNullFields*(T: type DefaultFlavor): bool = false
|
||||
|
||||
var DefaultFlavorEnumRep {.compileTime.} = EnumAsString
|
||||
template flavorEnumRep*(T: type DefaultFlavor): EnumRepresentation =
|
||||
DefaultFlavorEnumRep
|
||||
|
||||
template flavorEnumRep*(T: type DefaultFlavor, rep: static[EnumRepresentation]) =
|
||||
static:
|
||||
DefaultFlavorEnumRep = rep
|
||||
|
||||
# If user choose to use `Json` instead of `DefaultFlavor`, it still goes to `DefaultFlavor`
|
||||
template flavorEnumRep*(T: type Json, rep: static[EnumRepresentation]) =
|
||||
static:
|
||||
DefaultFlavorEnumRep = rep
|
||||
|
||||
# We create overloads of these traits to force the mixin treatment of the symbols
|
||||
type DummyFlavor* = object
|
||||
template flavorUsesAutomaticObjectSerialization*(T: type DummyFlavor): bool = true
|
||||
|
@ -53,3 +72,11 @@ template createJsonFlavor*(FlavorName: untyped,
|
|||
template flavorRequiresAllFields*(T: type FlavorName): bool = requireAllFields
|
||||
template flavorAllowsUnknownFields*(T: type FlavorName): bool = allowUnknownFields
|
||||
template flavorSkipNullFields*(T: type FlavorName): bool = skipNullFields
|
||||
|
||||
var `FlavorName EnumRep` {.compileTime.} = EnumAsString
|
||||
template flavorEnumRep*(T: type FlavorName): EnumRepresentation =
|
||||
`FlavorName EnumRep`
|
||||
|
||||
template flavorEnumRep*(T: type FlavorName, rep: static[EnumRepresentation]) =
|
||||
static:
|
||||
`FlavorName EnumRep` = rep
|
||||
|
|
|
@ -271,6 +271,24 @@ proc writeJsonValueRef*[F,T](w: var JsonWriter[F], value: JsonValueRef[T]) =
|
|||
of JsonValueKind.Null:
|
||||
append "null"
|
||||
|
||||
template writeEnumImpl(w: var JsonWriter, value, enumRep) =
|
||||
mixin writeValue
|
||||
when enumRep == EnumAsString:
|
||||
w.writeValue $value
|
||||
elif enumRep == EnumAsNumber:
|
||||
w.stream.writeText(value.int)
|
||||
elif enumRep == EnumAsStringifiedNumber:
|
||||
w.writeValue $value.int
|
||||
|
||||
template writeValue*(w: var JsonWriter, value: enum) =
|
||||
# We extract this as a template because
|
||||
# if we put it into `proc writeValue` below
|
||||
# the Nim compiler generic cache mechanism
|
||||
# will mess up with the compile time
|
||||
# conditional selection
|
||||
type Flavor = type(w).Flavor
|
||||
writeEnumImpl(w, value, Flavor.flavorEnumRep())
|
||||
|
||||
proc writeValue*(w: var JsonWriter, value: auto) {.gcsafe, raises: [IOError].} =
|
||||
mixin writeValue
|
||||
|
||||
|
@ -333,9 +351,6 @@ proc writeValue*(w: var JsonWriter, value: auto) {.gcsafe, raises: [IOError].} =
|
|||
elif value is bool:
|
||||
append if value: "true" else: "false"
|
||||
|
||||
elif value is enum:
|
||||
w.writeValue $value
|
||||
|
||||
elif value is range:
|
||||
when low(typeof(value)) < 0:
|
||||
w.stream.writeText int64(value)
|
||||
|
@ -384,3 +399,21 @@ proc toJson*(v: auto, pretty = false, typeAnnotations = false): string =
|
|||
template serializesAsTextInJson*(T: type[enum]) =
|
||||
template writeValue*(w: var JsonWriter, val: T) =
|
||||
w.writeValue $val
|
||||
|
||||
template configureJsonSerialization*(
|
||||
T: type[enum], enumRep: static[EnumRepresentation]) =
|
||||
proc writeValue*(w: var JsonWriter,
|
||||
value: T) {.gcsafe, raises: [IOError].} =
|
||||
writeEnumImpl(w, value, enumRep)
|
||||
|
||||
template configureJsonSerialization*(Flavor: type,
|
||||
T: type[enum],
|
||||
enumRep: static[EnumRepresentation]) =
|
||||
when Flavor is Json:
|
||||
proc writeValue*(w: var JsonWriter[DefaultFlavor],
|
||||
value: T) {.gcsafe, raises: [IOError].} =
|
||||
writeEnumImpl(w, value, enumRep)
|
||||
else:
|
||||
proc writeValue*(w: var JsonWriter[Flavor],
|
||||
value: T) {.gcsafe, raises: [IOError].} =
|
||||
writeEnumImpl(w, value, enumRep)
|
||||
|
|
|
@ -132,3 +132,71 @@ suite "Test JsonFlavor":
|
|||
# field should not processed at all
|
||||
let y = NullyFields.decode(jsonTextWithNullFields, ListOnly)
|
||||
check y.list.string.len == 0
|
||||
|
||||
test "Enum value representation primitives":
|
||||
when NullyFields.flavorEnumRep() == EnumAsString:
|
||||
check true
|
||||
elif NullyFields.flavorEnumRep() == EnumAsNumber:
|
||||
check false
|
||||
elif NullyFields.flavorEnumRep() == EnumAsStringifiedNumber:
|
||||
check false
|
||||
|
||||
NullyFields.flavorEnumRep(EnumAsNumber)
|
||||
when NullyFields.flavorEnumRep() == EnumAsString:
|
||||
check false
|
||||
elif NullyFields.flavorEnumRep() == EnumAsNumber:
|
||||
check true
|
||||
elif NullyFields.flavorEnumRep() == EnumAsStringifiedNumber:
|
||||
check false
|
||||
|
||||
NullyFields.flavorEnumRep(EnumAsStringifiedNumber)
|
||||
when NullyFields.flavorEnumRep() == EnumAsString:
|
||||
check false
|
||||
elif NullyFields.flavorEnumRep() == EnumAsNumber:
|
||||
check false
|
||||
elif NullyFields.flavorEnumRep() == EnumAsStringifiedNumber:
|
||||
check true
|
||||
|
||||
test "Enum value representation of custom flavor":
|
||||
type
|
||||
ExoticFruits = enum
|
||||
DragonFruit
|
||||
SnakeFruit
|
||||
StarFruit
|
||||
|
||||
NullyFields.flavorEnumRep(EnumAsNumber)
|
||||
let u = NullyFields.encode(DragonFruit)
|
||||
check u == "0"
|
||||
|
||||
NullyFields.flavorEnumRep(EnumAsString)
|
||||
let v = NullyFields.encode(SnakeFruit)
|
||||
check v == "\"SnakeFruit\""
|
||||
|
||||
NullyFields.flavorEnumRep(EnumAsStringifiedNumber)
|
||||
let w = NullyFields.encode(StarFruit)
|
||||
check w == "\"2\""
|
||||
|
||||
test "EnumAsString of custom flavor":
|
||||
type
|
||||
Fruit = enum
|
||||
Banana = "BaNaNa"
|
||||
Apple = "ApplE"
|
||||
JackFruit = "VVV"
|
||||
|
||||
NullyFields.flavorEnumRep(EnumAsString)
|
||||
let u = NullyFields.encode(Banana)
|
||||
check u == "\"BaNaNa\""
|
||||
|
||||
let v = NullyFields.encode(Apple)
|
||||
check v == "\"ApplE\""
|
||||
|
||||
let w = NullyFields.encode(JackFruit)
|
||||
check w == "\"VVV\""
|
||||
|
||||
NullyFields.flavorEnumRep(EnumAsStringifiedNumber)
|
||||
let x = NullyFields.encode(JackFruit)
|
||||
check x == "\"2\""
|
||||
|
||||
NullyFields.flavorEnumRep(EnumAsNumber)
|
||||
let z = NullyFields.encode(Banana)
|
||||
check z == "0"
|
|
@ -23,7 +23,7 @@ type
|
|||
a: Opt[int]
|
||||
b: Option[string]
|
||||
c: int
|
||||
|
||||
|
||||
createJsonFlavor YourJson,
|
||||
omitOptionalFields = false
|
||||
|
||||
|
@ -33,6 +33,20 @@ createJsonFlavor MyJson,
|
|||
ObjectWithOptionalFields.useDefaultSerializationIn YourJson
|
||||
ObjectWithOptionalFields.useDefaultSerializationIn MyJson
|
||||
|
||||
type
|
||||
FruitX = enum
|
||||
BananaX = "BaNaNa"
|
||||
AppleX = "ApplE"
|
||||
GrapeX = "VVV"
|
||||
|
||||
Drawer = enum
|
||||
One
|
||||
Two
|
||||
|
||||
FruitX.configureJsonSerialization(EnumAsString)
|
||||
Json.configureJsonSerialization(Drawer, EnumAsNumber)
|
||||
MyJson.configureJsonSerialization(Drawer, EnumAsString)
|
||||
|
||||
proc writeValue*(w: var JsonWriter, val: OWOF)
|
||||
{.gcsafe, raises: [IOError].} =
|
||||
w.writeObject(OWOF):
|
||||
|
@ -168,3 +182,94 @@ suite "Test writer":
|
|||
check uu.string == """{"a":123,"b":"nano","c":456}"""
|
||||
let vv = YourJson.encode(y)
|
||||
check vv.string == """{"a":null,"b":null,"c":999}"""
|
||||
|
||||
test "Enum value representation primitives":
|
||||
when DefaultFlavor.flavorEnumRep() == EnumAsString:
|
||||
check true
|
||||
elif DefaultFlavor.flavorEnumRep() == EnumAsNumber:
|
||||
check false
|
||||
elif DefaultFlavor.flavorEnumRep() == EnumAsStringifiedNumber:
|
||||
check false
|
||||
|
||||
DefaultFlavor.flavorEnumRep(EnumAsNumber)
|
||||
when DefaultFlavor.flavorEnumRep() == EnumAsString:
|
||||
check false
|
||||
elif DefaultFlavor.flavorEnumRep() == EnumAsNumber:
|
||||
check true
|
||||
elif DefaultFlavor.flavorEnumRep() == EnumAsStringifiedNumber:
|
||||
check false
|
||||
|
||||
DefaultFlavor.flavorEnumRep(EnumAsStringifiedNumber)
|
||||
when DefaultFlavor.flavorEnumRep() == EnumAsString:
|
||||
check false
|
||||
elif DefaultFlavor.flavorEnumRep() == EnumAsNumber:
|
||||
check false
|
||||
elif DefaultFlavor.flavorEnumRep() == EnumAsStringifiedNumber:
|
||||
check true
|
||||
|
||||
test "Enum value representation of DefaultFlavor":
|
||||
type
|
||||
ExoticFruits = enum
|
||||
DragonFruit
|
||||
SnakeFruit
|
||||
StarFruit
|
||||
|
||||
DefaultFlavor.flavorEnumRep(EnumAsNumber)
|
||||
let u = Json.encode(DragonFruit)
|
||||
check u == "0"
|
||||
|
||||
DefaultFlavor.flavorEnumRep(EnumAsString)
|
||||
let v = Json.encode(SnakeFruit)
|
||||
check v == "\"SnakeFruit\""
|
||||
|
||||
DefaultFlavor.flavorEnumRep(EnumAsStringifiedNumber)
|
||||
let w = Json.encode(StarFruit)
|
||||
check w == "\"2\""
|
||||
|
||||
test "EnumAsString of DefaultFlavor/Json":
|
||||
type
|
||||
Fruit = enum
|
||||
Banana = "BaNaNa"
|
||||
Apple = "ApplE"
|
||||
JackFruit = "VVV"
|
||||
|
||||
ObjectWithEnumField = object
|
||||
fruit: Fruit
|
||||
|
||||
Json.flavorEnumRep(EnumAsString)
|
||||
let u = Json.encode(Banana)
|
||||
check u == "\"BaNaNa\""
|
||||
|
||||
let v = Json.encode(Apple)
|
||||
check v == "\"ApplE\""
|
||||
|
||||
let w = Json.encode(JackFruit)
|
||||
check w == "\"VVV\""
|
||||
|
||||
Json.flavorEnumRep(EnumAsStringifiedNumber)
|
||||
let x = Json.encode(JackFruit)
|
||||
check x == "\"2\""
|
||||
|
||||
Json.flavorEnumRep(EnumAsNumber)
|
||||
let z = Json.encode(Banana)
|
||||
check z == "0"
|
||||
|
||||
let obj = ObjectWithEnumField(fruit: Banana)
|
||||
let zz = Json.encode(obj)
|
||||
check zz == """{"fruit":0}"""
|
||||
|
||||
test "Individual enum configuration":
|
||||
Json.flavorEnumRep(EnumAsNumber)
|
||||
# Although the flavor config is EnumAsNumber
|
||||
# FruitX is configured as EnumAsAstring
|
||||
let z = Json.encode(BananaX)
|
||||
check z == "\"BaNaNa\""
|
||||
|
||||
# configuration: Json.configureJsonSerialization(Drawer, EnumAsNumber)
|
||||
let u = Json.encode(Two)
|
||||
check u == "1"
|
||||
|
||||
# configuration: MyJson.configureJsonSerialization(Drawer, EnumAsString)
|
||||
let v = MyJson.encode(One)
|
||||
check v == "\"One\""
|
||||
|
Loading…
Reference in New Issue