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:
andri lim 2024-07-27 07:27:11 +07:00 committed by GitHub
parent 6ff807654f
commit 8a4ed98bbd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 296 additions and 4 deletions

View File

@ -261,6 +261,65 @@ writeNumber[F,T](w: var JsonWriter[F], value: JsonNumber[T])
writeJsonValueRef[F,T](w: var JsonWriter[F], value: JsonValueRef[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 ## License
Licensed and distributed under either of Licensed and distributed under either of

View File

@ -20,12 +20,31 @@ template supports*(_: type Json, T: type): bool =
# The JSON format should support every type # The JSON format should support every type
true true
type
EnumRepresentation* = enum
EnumAsString
EnumAsNumber
EnumAsStringifiedNumber
template flavorUsesAutomaticObjectSerialization*(T: type DefaultFlavor): bool = true template flavorUsesAutomaticObjectSerialization*(T: type DefaultFlavor): bool = true
template flavorOmitsOptionalFields*(T: type DefaultFlavor): bool = true template flavorOmitsOptionalFields*(T: type DefaultFlavor): bool = true
template flavorRequiresAllFields*(T: type DefaultFlavor): bool = false template flavorRequiresAllFields*(T: type DefaultFlavor): bool = false
template flavorAllowsUnknownFields*(T: type DefaultFlavor): bool = false template flavorAllowsUnknownFields*(T: type DefaultFlavor): bool = false
template flavorSkipNullFields*(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 # We create overloads of these traits to force the mixin treatment of the symbols
type DummyFlavor* = object type DummyFlavor* = object
template flavorUsesAutomaticObjectSerialization*(T: type DummyFlavor): bool = true template flavorUsesAutomaticObjectSerialization*(T: type DummyFlavor): bool = true
@ -53,3 +72,11 @@ template createJsonFlavor*(FlavorName: untyped,
template flavorRequiresAllFields*(T: type FlavorName): bool = requireAllFields template flavorRequiresAllFields*(T: type FlavorName): bool = requireAllFields
template flavorAllowsUnknownFields*(T: type FlavorName): bool = allowUnknownFields template flavorAllowsUnknownFields*(T: type FlavorName): bool = allowUnknownFields
template flavorSkipNullFields*(T: type FlavorName): bool = skipNullFields 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

View File

@ -271,6 +271,24 @@ proc writeJsonValueRef*[F,T](w: var JsonWriter[F], value: JsonValueRef[T]) =
of JsonValueKind.Null: of JsonValueKind.Null:
append "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].} = proc writeValue*(w: var JsonWriter, value: auto) {.gcsafe, raises: [IOError].} =
mixin writeValue mixin writeValue
@ -333,9 +351,6 @@ proc writeValue*(w: var JsonWriter, value: auto) {.gcsafe, raises: [IOError].} =
elif value is bool: elif value is bool:
append if value: "true" else: "false" append if value: "true" else: "false"
elif value is enum:
w.writeValue $value
elif value is range: elif value is range:
when low(typeof(value)) < 0: when low(typeof(value)) < 0:
w.stream.writeText int64(value) w.stream.writeText int64(value)
@ -384,3 +399,21 @@ proc toJson*(v: auto, pretty = false, typeAnnotations = false): string =
template serializesAsTextInJson*(T: type[enum]) = template serializesAsTextInJson*(T: type[enum]) =
template writeValue*(w: var JsonWriter, val: T) = template writeValue*(w: var JsonWriter, val: T) =
w.writeValue $val 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)

View File

@ -132,3 +132,71 @@ suite "Test JsonFlavor":
# field should not processed at all # field should not processed at all
let y = NullyFields.decode(jsonTextWithNullFields, ListOnly) let y = NullyFields.decode(jsonTextWithNullFields, ListOnly)
check y.list.string.len == 0 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"

View File

@ -23,7 +23,7 @@ type
a: Opt[int] a: Opt[int]
b: Option[string] b: Option[string]
c: int c: int
createJsonFlavor YourJson, createJsonFlavor YourJson,
omitOptionalFields = false omitOptionalFields = false
@ -33,6 +33,20 @@ createJsonFlavor MyJson,
ObjectWithOptionalFields.useDefaultSerializationIn YourJson ObjectWithOptionalFields.useDefaultSerializationIn YourJson
ObjectWithOptionalFields.useDefaultSerializationIn MyJson 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) proc writeValue*(w: var JsonWriter, val: OWOF)
{.gcsafe, raises: [IOError].} = {.gcsafe, raises: [IOError].} =
w.writeObject(OWOF): w.writeObject(OWOF):
@ -168,3 +182,94 @@ suite "Test writer":
check uu.string == """{"a":123,"b":"nano","c":456}""" check uu.string == """{"a":123,"b":"nano","c":456}"""
let vv = YourJson.encode(y) let vv = YourJson.encode(y)
check vv.string == """{"a":null,"b":null,"c":999}""" 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\""