envvar encoder decoder implementation

This commit is contained in:
jangko 2020-10-22 14:18:08 +07:00 committed by zah
parent cf1d2313d8
commit a263d76bc0
6 changed files with 375 additions and 1 deletions

View File

@ -0,0 +1,50 @@
import
stew/shims/macros,
serialization, ./reader, ./writer, ./utils
export
serialization, reader, writer, utils
serializationFormat Envvar,
Reader = EnvvarReader,
Writer = EnvvarWriter,
PreferedOutput = void
template supports*(_: type Envvar, T: type): bool =
# The Envvar format should support every type
true
template decode*(_: type Envvar,
prefix: string,
RecordType: distinct type,
params: varargs[untyped]): auto =
mixin init, ReaderType
{.noSideEffect.}:
var reader = unpackArgs(init, [EnvvarReader, prefix, params])
reader.readValue(RecordType)
template encode*(_: type Envvar,
prefix: string,
value: auto,
params: varargs[untyped]) =
mixin init, WriterType, writeValue
{.noSideEffect.}:
var writer = unpackArgs(init, [EnvvarWriter, prefix, params])
writeValue writer, value
template loadFile*(_: type Envvar,
prefix: string,
RecordType: distinct type,
params: varargs[untyped]): auto =
mixin init, ReaderType, readValue
var reader = unpackArgs(init, [EnvvarReader, prefix, params])
reader.readValue(RecordType)
template saveFile*(Format: type, prefix: string, value: auto, params: varargs[untyped]) =
mixin init, WriterType, writeValue
var writer = unpackArgs(init, [EnvvarWriter, prefix, params])
writer.writeValue(value)

View File

@ -0,0 +1,86 @@
import
tables, typetraits, options, os,
serialization/[object_serialization, errors],
./utils
type
EnvvarReader* = object
prefix: string
key: seq[string]
EnvvarError* = object of SerializationError
EnvvarReaderError* = object of EnvvarError
GenericEnvvarReaderError* = object of EnvvarReaderError
deserializedField*: string
innerException*: ref CatchableError
proc handleReadException*(r: EnvvarReader,
Record: type,
fieldName: string,
field: auto,
err: ref CatchableError) =
var ex = new GenericEnvvarReaderError
ex.deserializedField = fieldName
ex.innerException = err
raise ex
proc init*(T: type EnvvarReader, prefix: string): T =
result.prefix = prefix
template getUnderlyingType*[T](_: Option[T]): untyped = T
proc readValue*[T](r: var EnvvarReader, value: var T)
{.raises: [SerializationError, ValueError, Defect].} =
mixin readValue
# TODO: reduce allocation
when T is (SomePrimitives or range or string):
let key = constructKey(r.prefix, r.key)
getValue(key, value)
elif T is Option:
let key = constructKey(r.prefix, r.key)
if existsEnv(key):
var outVal: getUnderlyingType(value)
getValue(key, outVal)
value = some(outVal)
elif T is (seq or array):
when uTypeIsPrimitives(T):
let key = constructKey(r.prefix, r.key)
getValue(key, value)
elif uTypeIsRecord(T):
let key = r.key[^1]
for i in 0..<value.len:
r.key[^1] = key & $i
r.readValue(value[i])
else:
const typeName = typetraits.name(T)
{.fatal: "Failed to convert from Envvar array an unsupported type: " & typeName.}
elif T is (object or tuple):
type T = type(value)
when T.totalSerializedFields > 0:
let fields = T.fieldReadersTable(EnvvarReader)
var expectedFieldPos = 0
r.key.add ""
value.enumInstanceSerializedFields(fieldName, field):
when T is tuple:
r.key[^1] = $expectedFieldPos
var reader = fields[][expectedFieldPos].reader
expectedFieldPos += 1
else:
r.key[^1] = fieldName
var reader = findFieldReader(fields[], fieldName, expectedFieldPos)
if reader != nil:
reader(value, r)
discard r.key.pop()
else:
const typeName = typetraits.name(T)
{.fatal: "Failed to convert from Envvar an unsupported type: " & typeName.}

View File

@ -0,0 +1,93 @@
import
os,
stew/byteutils, stew/ranges/ptr_arith
type
SomePrimitives* = SomeInteger | enum | bool | SomeFloat | char
proc setValue*[T: SomePrimitives](key: string, val: openArray[T]) =
os.putEnv(key, byteutils.toHex(makeOpenArray(val[0].unsafeAddr, byte, val.len*sizeof(T))))
proc setValue*(key: string, val: SomePrimitives) =
os.putEnv(key, byteutils.toHex(makeOpenArray(val.unsafeAddr, byte, sizeof(val))))
proc decodePaddedHex(hex: string, res: ptr UncheckedArray[byte], outputLen: int) =
# make it an even length
let
inputLen = hex.len and not 0x01
numHex = inputLen div 2
maxLen = min(outputLen, numHex)
var
offI = hex.len - maxLen * 2
offO = outputLen - maxLen
for i in 0 ..< maxLen:
res[i + offO] = hex[2*i + offI].readHexChar shl 4 or hex[2*i + 1 + offI].readHexChar
# write single nibble from odd length hex
if (offO > 0) and (offI > 0):
res[offO-1] = hex[offI-1].readHexChar
proc getValue*(key: string, outVal: var string) =
let hex = os.getEnv(key)
let size = (hex.len div 2) + (hex.len and 0x01)
outVal.setLen(size)
decodePaddedHex(hex, cast[ptr UncheckedArray[byte]](outVal[0].addr), size)
proc getValue*[T: SomePrimitives](key: string, outVal: var seq[T]) =
let hex = os.getEnv(key)
let byteSize = (hex.len div 2) + (hex.len and 0x01)
let size = (byteSize + sizeof(T) - 1) div sizeof(T)
outVal.setLen(size)
decodePaddedHex(hex, cast[ptr UncheckedArray[byte]](outVal[0].addr), size * sizeof(T))
proc getValue*[N, T: SomePrimitives](key: string, outVal: var array[N, T]) =
let hex = os.getEnv(key)
decodePaddedHex(hex, cast[ptr UncheckedArray[byte]](outVal[0].addr), sizeof(outVal))
proc getValue*(key: string, outVal: var SomePrimitives) =
let hex = os.getEnv(key)
decodePaddedHex(hex, cast[ptr UncheckedArray[byte]](outVal.addr), sizeof(outVal))
template uTypeIsPrimitives*[T](_: type seq[T]): bool =
when T is SomePrimitives:
true
else:
false
template uTypeIsPrimitives*[N, T](_: type array[N, T]): bool =
when T is SomePrimitives:
true
else:
false
template uTypeIsPrimitives*[T](_: type openArray[T]): bool =
when T is SomePrimitives:
true
else:
false
template uTypeIsRecord*(_: typed): bool =
false
template uTypeIsRecord*[T](_: type seq[T]): bool =
when T is (object or tuple):
true
else:
false
template uTypeIsRecord*[N, T](_: type array[N, T]): bool =
when T is (object or tuple):
true
else:
false
func constructKey*(prefix: string, keys: openArray[string]): string =
var size = prefix.len
for i in 0..<keys.len:
inc(size, keys[i].len)
result = newStringOfCap(size)
result.add prefix
for x in keys:
result.add x

View File

@ -0,0 +1,52 @@
import
typetraits, options, tables,
serialization,
./utils
type
EnvvarWriter* = object
prefix: string
key: seq[string]
proc init*(T: type EnvvarWriter, prefix: string): T =
result.prefix = prefix
proc writeValue*(w: var EnvvarWriter, value: auto) =
mixin enumInstanceSerializedFields, writeValue, writeFieldIMPL
# TODO: reduce allocation
when value is (SomePrimitives or range or string):
let key = constructKey(w.prefix, w.key)
setValue(key, value)
elif value is Option:
if value.isSome:
w.writeValue value.get
elif value is (seq or array or openArray):
when uTypeIsPrimitives(type value):
let key = constructKey(w.prefix, w.key)
setValue(key, value)
elif uTypeIsRecord(type value):
let key = w.key[^1]
for i in 0..<value.len:
w.key[^1] = key & $i
w.writeValue(value[i])
else:
const typeName = typetraits.name(value.type)
{.fatal: "Failed to convert to Envvar array an unsupported type: " & typeName.}
elif value is (object or tuple):
type RecordType = type value
w.key.add ""
value.enumInstanceSerializedFields(fieldName, field):
type FieldType = type field
w.key[^1] = fieldName
w.writeFieldIMPL(FieldTag[RecordType, fieldName, FieldType], field, value)
discard w.key.pop()
else:
const typeName = typetraits.name(value.type)
{.fatal: "Failed to convert to Envvar an unsupported type: " & typeName.}

View File

@ -8,4 +8,5 @@
{. warning[UnusedImport]:off .}
import
test_config_file
test_config_file,
test_envvar

92
tests/test_envvar.nim Normal file
View File

@ -0,0 +1,92 @@
import
unittest, options,
../confutils/envvar/envvar_serialization,
../confutils/envvar/utils
const
commonPrefix = "Nimbus"
template readWrite(key: string, val: typed) =
test key:
setValue(key, val)
var outVal: type val
getValue(key, outVal)
check outVal == val
proc testUtils() =
type
Fruit = enum
Apple
suite "envvar utils test suite":
readWrite("some number", 123'u32)
readWrite("some number 64", 123'u64)
readWrite("some bytes", @[1.byte, 2.byte])
readWrite("some int list", @[4,5,6])
readWrite("some array", [1.byte, 2.byte, 4.byte])
readWrite("some string", "hello world")
readWrite("some enum", Apple)
readWrite("some boolean", true)
readWrite("some float32", 1.234'f32)
readWrite("some float64", 1.234'f64)
proc testEncoder() =
type
Class = enum
Truck
MPV
SUV
Fuel = enum
Gasoline
Diesel
Engine = object
cylinder: int
valve: int16
fuel: Fuel
Suspension = object
dist: int
length: int
Vehicle = object
name: string
color: int
class: Class
engine: Engine
wheel: int
suspension: array[3, Suspension]
door: array[4, int]
antennae: Option[int]
bumper: Option[string]
suite "envvar encoder test suite":
test "basic encoder and decoder":
let v = Vehicle(
name: "buggy",
color: 213,
class: MPV,
engine: Engine(
cylinder: 3,
valve: 2,
fuel: Diesel
),
wheel: 6,
door: [1,2,3,4],
suspension: [
Suspension(dist: 1, length: 5),
Suspension(dist: 2, length: 6),
Suspension(dist: 3, length: 7)
],
bumper: some("Chromium")
)
Envvar.encode(commonPrefix, v)
let x = Envvar.decode(commonPrefix, Vehicle)
check x == v
check x.antennae.isNone
check x.bumper.get() == "Chromium"
testUtils()
testEncoder()