From 2028b41602b3abf7c9bf450744efde7b296707a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Uhl=C3=AD=C5=99?= Date: Wed, 19 Apr 2023 11:54:48 +0200 Subject: [PATCH] feat: integrate env. variable support natively (#70) --- README.md | 8 +- confutils.nim | 24 +++- confutils/envvar/envvar_serialization.nim | 50 ------- confutils/envvar/reader.nim | 86 ----------- confutils/envvar/utils.nim | 93 ------------ confutils/envvar/writer.nim | 54 ------- tests/test_config_file.nim | 23 +-- tests/test_envvar.nim | 166 ++++++++-------------- 8 files changed, 88 insertions(+), 416 deletions(-) delete mode 100644 confutils/envvar/envvar_serialization.nim delete mode 100644 confutils/envvar/reader.nim delete mode 100644 confutils/envvar/utils.nim delete mode 100644 confutils/envvar/writer.nim diff --git a/README.md b/README.md index a1ce420..67f1f0d 100644 --- a/README.md +++ b/README.md @@ -425,8 +425,12 @@ variables. The default format is called `CmdLineFormat` and it uses the same `parseCmdArg` calls responsible for parsing the command-line. The names of the environment variables are prefixed by the name of the -program by default. They are matched in case-insensitive fashion and -certain characters such as `-` and `_` are ignored. +program by default and joined with the name of command line option, which is +uppercased and characters `-` and spaces are replaced with underscore: + +```nim +let env_variable_name = &"{prefix}_{key}".toUpperAscii.multiReplace(("-", "_"), (" ", "_")) +``` #### `configFileEnumerator` diff --git a/confutils.nim b/confutils.nim index 083d079..dcf6ec4 100644 --- a/confutils.nim +++ b/confutils.nim @@ -1,5 +1,6 @@ import - std/[options, strutils, wordwrap], + os, + std/[options, strutils, wordwrap, strformat], stew/shims/macros, serialization, confutils/[defs, cli_parser, config_file] @@ -862,6 +863,12 @@ proc addConfigFileContent*(secondarySources: auto, except IOError: raiseAssert "This should not be possible" +func constructEnvKey*(prefix: string, key: string): string = + ## Generates env. variable names from keys and prefix following the + ## IEEE Open Group env. variable spec: https://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html + + return (&"{prefix}_{key}").toUpperAscii.multiReplace(("-", "_"), (" ", "_")) + proc loadImpl[C, SecondarySources]( Configuration: typedesc[C], cmdLine = commandLineParams(), @@ -871,7 +878,8 @@ proc loadImpl[C, SecondarySources]( quitOnFailure = true, secondarySourcesRef: ref SecondarySources, secondarySources: proc (config: Configuration, - sources: ref SecondarySources) = nil): Configuration = + sources: ref SecondarySources) = nil, + envVarsPrefix = getAppFilename()): Configuration = ## Loads a program configuration by parsing command-line arguments ## and a standard set of config files that can specify: ## @@ -1105,7 +1113,12 @@ proc loadImpl[C, SecondarySources]( proc processMissingOpts(conf: var Configuration, cmd: CmdInfo) = for opt in cmd.opts: if fieldCounters[opt.idx] == 0: - if secondarySourcesRef.setters[opt.idx](conf, secondarySourcesRef): + let envKey = constructEnvKey(envVarsPrefix, opt.name) + + if existsEnv(envKey): + let envContent = getEnv(envKey) + applySetter(opt.idx, envContent) + elif secondarySourcesRef.setters[opt.idx](conf, secondarySourcesRef): # all work is done in the config file setter, # there is nothing left to do here. discard @@ -1124,13 +1137,14 @@ template load*( copyrightBanner = "", printUsage = true, quitOnFailure = true, - secondarySources: untyped = nil): untyped = + secondarySources: untyped = nil, + envVarsPrefix = getAppFilename()): untyped = block: var secondarySourcesRef = generateSecondarySources(Configuration) loadImpl(Configuration, cmdLine, version, copyrightBanner, printUsage, quitOnFailure, - secondarySourcesRef, secondarySources) + secondarySourcesRef, secondarySources, envVarsPrefix) func defaults*(Configuration: type): Configuration = load(Configuration, cmdLine = @[], printUsage = false, quitOnFailure = false) diff --git a/confutils/envvar/envvar_serialization.nim b/confutils/envvar/envvar_serialization.nim deleted file mode 100644 index 18f37e0..0000000 --- a/confutils/envvar/envvar_serialization.nim +++ /dev/null @@ -1,50 +0,0 @@ -import - stew/shims/macros, - serialization, ./reader, ./writer - -export - serialization, reader, writer - -serializationFormat Envvar - -Envvar.setReader EnvvarReader -Envvar.setWriter EnvvarWriter, PreferredOutput = 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*(_: type Envvar, 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 deleted file mode 100644 index 2545625..0000000 --- a/confutils/envvar/reader.nim +++ /dev/null @@ -1,86 +0,0 @@ -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 - -proc readValue*[T](r: var EnvvarReader, value: var T) = - mixin readValue - # TODO: reduce allocation - - when T is string: - let key = constructKey(r.prefix, r.key) - value = os.getEnv(key) - - elif T is (SomePrimitives or range): - let key = constructKey(r.prefix, r.key) - getValue(key, value) - - elif T is Option: - template getUnderlyingType[T](_: Option[T]): untyped = T - let key = constructKey(r.prefix, r.key) - if existsEnv(key): - type uType = getUnderlyingType(value) - when uType is string: - value = some(os.getEnv(key)) - else: - value = some(r.readValue(uType)) - - elif T is (seq or array): - when uTypeIsPrimitives(T): - let key = constructKey(r.prefix, r.key) - getValue(key, value) - - else: - 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 deleted file mode 100644 index 4da98d1..0000000 --- a/confutils/envvar/utils.nim +++ /dev/null @@ -1,93 +0,0 @@ -import - os, - stew/byteutils, stew/ptrops - -type - SomePrimitives* = SomeInteger | enum | bool | SomeFloat | char - -proc setValue*[T: SomePrimitives](key: string, val: openArray[T]) = - os.putEnv(key, byteutils.toHex(makeOpenArray(baseAddr val, 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..