From a263d76bc05b5064ddb7eeadfd016de914bd12ad Mon Sep 17 00:00:00 2001 From: jangko Date: Thu, 22 Oct 2020 14:18:08 +0700 Subject: [PATCH] envvar encoder decoder implementation --- confutils/envvar/envvar_serialization.nim | 50 ++++++++++++ confutils/envvar/reader.nim | 86 +++++++++++++++++++++ confutils/envvar/utils.nim | 93 +++++++++++++++++++++++ confutils/envvar/writer.nim | 52 +++++++++++++ tests/test_all.nim | 3 +- tests/test_envvar.nim | 92 ++++++++++++++++++++++ 6 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 confutils/envvar/envvar_serialization.nim create mode 100644 confutils/envvar/reader.nim create mode 100644 confutils/envvar/utils.nim create mode 100644 confutils/envvar/writer.nim create mode 100644 tests/test_envvar.nim diff --git a/confutils/envvar/envvar_serialization.nim b/confutils/envvar/envvar_serialization.nim new file mode 100644 index 0000000..e0cd6ff --- /dev/null +++ b/confutils/envvar/envvar_serialization.nim @@ -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) diff --git a/confutils/envvar/reader.nim b/confutils/envvar/reader.nim new file mode 100644 index 0000000..3b86a90 --- /dev/null +++ b/confutils/envvar/reader.nim @@ -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.. 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.} diff --git a/confutils/envvar/utils.nim b/confutils/envvar/utils.nim new file mode 100644 index 0000000..4e05155 --- /dev/null +++ b/confutils/envvar/utils.nim @@ -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..