envvar encoder decoder implementation
This commit is contained in:
parent
cf1d2313d8
commit
a263d76bc0
|
@ -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)
|
|
@ -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.}
|
|
@ -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
|
|
@ -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.}
|
|
@ -8,4 +8,5 @@
|
|||
{. warning[UnusedImport]:off .}
|
||||
|
||||
import
|
||||
test_config_file
|
||||
test_config_file,
|
||||
test_envvar
|
||||
|
|
|
@ -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()
|
Loading…
Reference in New Issue