diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 4417fbd..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(swift build:*)", - "Bash(grep:*)" - ] - } -} diff --git a/.gitignore b/.gitignore index 9ae82d8..e813749 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ DerivedData/ .swiftpm/configuration/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc +__pycache__/ Open Sourcing/ +.claude/settings.local.json diff --git a/Documentation/CodeGeneration.md b/Documentation/CodeGeneration.md new file mode 100644 index 0000000..c3a20ed --- /dev/null +++ b/Documentation/CodeGeneration.md @@ -0,0 +1,167 @@ +# Code Generation with GYB + +Some source files in DevConfiguration are generated using +[GYB][GYB] (Generate Your Boilerplate), the same template tool used by +[swift-configuration][SwiftConfiguration]. GYB lets us define repetitive API patterns once in a +template and generate type-specific overloads automatically, keeping the codebase consistent and +reducing the maintenance burden of boilerplate code. + +[GYB]: https://github.com/apple/swift/blob/main/utils/gyb.py +[SwiftConfiguration]: https://github.com/apple/swift-configuration + + +## Why GYB? + +`ConfigVariableReader` exposes `value(for:)`, `fetchValue(for:)`, `watchValue(for:)`, and +subscript accessors for every supported value type: `Bool`, `Float64`, `Int`, `String`, +`[UInt8]`, their array variants, and generic overloads for `RawRepresentable` and +`ExpressibleByConfig*` types. Each overload follows the same structure but delegates to a +different `ConfigReader` method. Writing and maintaining these by hand would be error-prone and +tedious. GYB lets us define the pattern once and generate all the overloads from a shared list +of type definitions. + + +## File Structure + + Scripts/ + generate-gyb # Entry point script; regenerates all .gyb templates + gyb/ + gyb.py # The GYB tool itself (from the Swift project) + gyb_utils.py # Shared type definitions and helper functions + + Sources/DevConfiguration/Core/ + ConfigVariableReader+Functions.swift.gyb # Source template + ConfigVariableReader+Functions.swift # Generated output (checked in) + + Tests/DevConfigurationTests/Unit Tests/Core/ + ConfigVariableReader+FunctionsTests.swift.gyb # Test template + ConfigVariableReader+FunctionsTests.swift # Generated output (checked in) + +The generated `.swift` files are checked in so that contributors can build and test without +needing to run GYB. The `.gyb` templates are the source of truth. + + +## GYB Template Syntax + +GYB templates are plain text files (in our case, Swift code) with embedded Python. There are +three constructs: + +### Code Blocks (`%{ ... }%`) + +Multi-line Python code, typically used at the top of a template for imports and setup: + + %{ + import sys + import os + + scripts_dir = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + '..', '..', '..', 'Scripts', 'gyb' + ) + sys.path.insert(0, scripts_dir) + + from gyb_utils import * + }% + +### Line Directives (`%`) + +Single-line Python statements, used for loops and conditionals. These must start at the +beginning of the line: + + % for i, p in enumerate(primitive_types): + % if i > 0: + + % end + public func value(for variable: ConfigVariable<${p["type"]}>) -> ${p["type"]} { + // ... + } + % end + +### Substitutions (`${ ... }`) + +Inline Python expressions that are evaluated and inserted into the output: + + /// Gets the value for the specified `${p["name"]}` config variable. + public func value( + for variable: ConfigVariable<${p["type"]}>, + // ... + ) -> ${p["type"]} { + +Note that `% end` closes both `for` and `if` blocks. GYB uses indentation-independent `end` +markers rather than Python's indentation-based scoping. + + +## Type Definitions in `gyb_utils.py` + +The `Scripts/gyb/gyb_utils.py` file defines the type lists that drive code generation. Each +list is an array of dictionaries, where each dictionary describes one type or type category: + +### `primitive_types` + +Types with dedicated `ConfigReader` methods (`bool`, `int`, `double`, `string`, `bytes`, +and their array variants). Each entry has: + + - **`name`**: Display name for doc comments (e.g., `"Bool"`, `"[String]"`) + - **`type`**: The Swift type used in signatures (e.g., `"Bool"`, `"[String]"`) + - **`reader_method`**: The `ConfigReader` method name (e.g., `"bool"`, `"stringArray"`) + +### `string_convertible_types` + +Generic types that use `ConfigReader.string(forKey:as:...)` and +`ConfigReader.stringArray(forKey:as:...)`. Each entry has: + + - **`protocol`**: The Swift protocol constraint (e.g., `"RawRepresentable"`) + - **`doc_name`**: Display name for doc comments + +### `int_convertible_types` + +Generic types that use `ConfigReader.int(forKey:as:...)` and +`ConfigReader.intArray(forKey:as:...)`. Same structure as `string_convertible_types`. + +### Helper Functions + + - **`autogenerated_warning()`**: Returns the "DO NOT EDIT" header comment for generated files + - **`upper_first(s)`**: Capitalizes the first character of a string, used to construct + method names like `fetchBool` from `bool` + + +## How to Make Changes + +### Editing Existing Overloads + +If you need to change the implementation pattern shared by all overloads (e.g., adding a new +parameter, changing how `isSecret` is passed): + + 1. Edit the `.gyb` template file + 2. Run `Scripts/generate-gyb` + 3. Review the generated `.swift` file to verify the changes + +### Adding a New Value Type + +If swift-configuration adds a new primitive type: + + 1. Add an entry to the appropriate list in `Scripts/gyb/gyb_utils.py` + 2. If adding test support, add a corresponding entry to the test template's helper list + 3. Run `Scripts/generate-gyb` + 4. Review the generated files + +### Adding a New API Pattern + +If you need a new category of methods (beyond get, fetch, watch, and subscript): + + 1. Add a new `% for` loop section in the source `.gyb` template + 2. If adding tests, add entries to the `api_tests` list in the test `.gyb` template + 3. Run `Scripts/generate-gyb` + 4. Review the generated files + +### Important Rules + + - **Never edit generated `.swift` files directly.** Your changes will be overwritten the + next time someone runs `Scripts/generate-gyb`. Always edit the `.gyb` template instead. + - **Always regenerate after changes.** Run `Scripts/generate-gyb` after editing any `.gyb` + template or `gyb_utils.py`. + - **Review the diff.** Generated code can be subtle. Always review the generated output + to make sure the changes are what you expect. + - **Keep `gyb_utils.py` in sync.** The source and test templates both import from + `gyb_utils.py`. If you add a type to `gyb_utils.py`, make sure the test template + accounts for it too. diff --git a/Package.resolved b/Package.resolved index ef97c41..69ffadd 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "80a17d18e0ccc6a214bff7f95c4a75b53a3047602341b244fe9313cd1ea0f425", + "originHash" : "ea344d91ca081a8d76646289b8dc1fab4bf021986aeb4261afa9052259b26704", "pins" : [ { "identity" : "devfoundation", @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-configuration", "state" : { - "revision" : "b4768bd68d8a6fb356bd372cb41905046244fcae", - "version" : "1.0.2" + "revision" : "1bb939fe7bbb00b8f8bab664cc90020c035c08d9", + "version" : "1.1.0" } }, { diff --git a/Package.swift b/Package.swift index 80409d3..5acc457 100644 --- a/Package.swift +++ b/Package.swift @@ -23,7 +23,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/apple/swift-configuration", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-configuration", from: "1.1.0"), .package(url: "https://github.com/DevKitOrganization/DevFoundation.git", from: "1.7.0"), .package(url: "https://github.com/DevKitOrganization/DevTesting", from: "1.5.0"), ], @@ -34,6 +34,9 @@ let package = Package( .product(name: "Configuration", package: "swift-configuration"), .product(name: "DevFoundation", package: "DevFoundation"), ], + exclude: [ + "Core/ConfigVariableReader+Functions.swift.gyb", + ], swiftSettings: swiftSettings ), .testTarget( @@ -42,6 +45,9 @@ let package = Package( "DevConfiguration", "DevTesting", ], + exclude: [ + "Unit Tests/Core/ConfigVariableReader+FunctionsTests.swift.gyb", + ], swiftSettings: swiftSettings ), ] diff --git a/README.md b/README.md index f6d29bc..d1b5ab7 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,26 @@ public interfaces are fully documented and tested. We aim for overall test cover To set up the development environment: - 1. Run `Scripts/install-git-hooks` to install pre-commit hooks that automatically check code - formatting. + 1. Run `Scripts/install-git-hooks` to install pre-commit hooks that automatically check + code formatting. 2. Use `Scripts/lint` to manually check code formatting at any time. 3. Use `Scripts/format` to automatically format code. +### Code Generation with GYB + +Some source files are generated using [GYB][GYB] (Generate Your Boilerplate) to reduce +repetitive boilerplate. Generated `.swift` files are checked in so that contributors don't +need to run GYB unless they modify a template. To regenerate after changing a `.gyb` template +or `Scripts/gyb/gyb_utils.py`: + + Scripts/generate-gyb + +Do not edit generated files directly — edit the `.gyb` template instead. See +[Documentation/CodeGeneration.md](Documentation/CodeGeneration.md) for details on template +syntax, type definitions, and workflows. + +[GYB]: https://github.com/apple/swift/blob/main/utils/gyb.py + ## Bugs and Feature Requests diff --git a/Scripts/generate-gyb b/Scripts/generate-gyb new file mode 100755 index 0000000..9b4c731 --- /dev/null +++ b/Scripts/generate-gyb @@ -0,0 +1,12 @@ +#!/bin/bash +set -eu + +SCRIPTS_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPTS_DIR/.." && pwd)" + +cd "$PROJECT_DIR" + +find . -name '*.gyb' | grep -v '.build/' | \ + while read -r file; do \ + "$SCRIPTS_DIR/gyb/gyb.py" --line-directive '' -o "${file%.gyb}" "$file"; \ + done diff --git a/Scripts/gyb/gyb.py b/Scripts/gyb/gyb.py new file mode 100755 index 0000000..7040108 --- /dev/null +++ b/Scripts/gyb/gyb.py @@ -0,0 +1,1259 @@ +#!/usr/bin/env python3 +# GYB: Generate Your Boilerplate (improved names welcome; at least +# this one's short). See -h output for instructions + +from __future__ import print_function + +import os +import re +import sys +import textwrap +import tokenize +from bisect import bisect + + +try: + from cStringIO import StringIO +except ImportError: + from io import StringIO + + +try: + basestring +except NameError: + basestring = str + + +def get_line_starts(s): + """Return a list containing the start index of each line in s. + + The list also contains a sentinel index for the end of the string, + so there will be one more element in the list than there are lines + in the string + """ + starts = [0] + + for line in s.split('\n'): + starts.append(starts[-1] + len(line) + 1) + + starts[-1] -= 1 + return starts + + +def strip_trailing_nl(s): + """If s ends with a newline, drop it; else return s intact""" + return s[:-1] if s.endswith('\n') else s + + +def split_lines(s): + """Split s into a list of lines, each of which has a trailing newline + + If the lines are later concatenated, the result is s, possibly + with a single appended newline. + """ + return [line + '\n' for line in s.split('\n')] + + +# text on a line up to the first '$$', '${', or '%%' +literalText = r'(?: [^$\n%] | \$(?![${]) | %(?!%) )*' + +# The part of an '%end' line that follows the '%' sign +linesClose = r'[\ \t]* end [\ \t]* (?: \# .* )? $' + +# Note: Where "# Absorb" appears below, the regexp attempts to eat up +# through the end of ${...} and %{...}% constructs. In reality we +# handle this with the Python tokenizer, which avoids mis-detections +# due to nesting, comments and strings. This extra absorption in the +# regexp facilitates testing the regexp on its own, by preventing the +# interior of some of these constructs from being treated as literal +# text. +tokenize_re = re.compile( + r''' +# %-lines and %{...}-blocks + # \n? # absorb one preceding newline + ^ + (?: + (?P + (?P<_indent> [\ \t]* % (?! [{%] ) [\ \t]* ) (?! [\ \t] | ''' + linesClose + r''' ) .* # noqa: E501 + ( \n (?P=_indent) (?! ''' + linesClose + r''' ) .* ) * + ) + | (?P [\ \t]* % [ \t]* ''' + linesClose + r''' ) + | [\ \t]* (?P %\{ ) + (?: [^}]| \} (?!%) )* \}% # Absorb + ) + \n? # absorb one trailing newline + +# Substitutions +| (?P \$\{ ) + [^}]* \} # Absorb + +# %% and $$ are literal % and $ respectively +| (?P[$%]) (?P=symbol) + +# Literal text +| (?P ''' + literalText + r''' + (?: + # newline that doesn't precede space+% + (?: \n (?! [\ \t]* %[^%] ) ) + ''' + literalText + r''' + )* + \n? + ) +''', re.VERBOSE | re.MULTILINE) + +gyb_block_close = re.compile(r'\}%[ \t]*\n?') + + +def token_pos_to_index(token_pos, start, line_starts): + """Translate a tokenize (line, column) pair into an absolute + position in source text given the position where we started + tokenizing and a list that maps lines onto their starting + character indexes. + """ + relative_token_line_plus1, token_col = token_pos + + # line number where we started tokenizing + start_line_num = bisect(line_starts, start) - 1 + + # line number of the token in the whole text + abs_token_line = relative_token_line_plus1 - 1 + start_line_num + + # if found in the first line, adjust the end column to account + # for the extra text + if relative_token_line_plus1 == 1: + token_col += start - line_starts[start_line_num] + + # Sometimes tokenizer errors report a line beyond the last one + if abs_token_line >= len(line_starts): + return line_starts[-1] + + return line_starts[abs_token_line] + token_col + + +def tokenize_python_to_unmatched_close_curly(source_text, start, line_starts): + """Apply Python's tokenize to source_text starting at index start + while matching open and close curly braces. When an unmatched + close curly brace is found, return its index. If not found, + return len(source_text). If there's a tokenization error, return + the position of the error. + """ + stream = StringIO(source_text) + stream.seek(start) + nesting = 0 + + try: + for kind, text, token_start, token_end, line_text \ + in tokenize.generate_tokens(stream.readline): + + if text == '{': + nesting += 1 + elif text == '}': + nesting -= 1 + if nesting < 0: + return token_pos_to_index(token_start, start, line_starts) + + except tokenize.TokenError as error: + (message, error_pos) = error.args + return token_pos_to_index(error_pos, start, line_starts) + + return len(source_text) + + +def tokenize_template(template_text): + r"""Given the text of a template, returns an iterator over + (tokenType, token, match) tuples. + + **Note**: this is template syntax tokenization, not Python + tokenization. + + When a non-literal token is matched, a client may call + iter.send(pos) on the iterator to reset the position in + template_text at which scanning will resume. + + This function provides a base level of tokenization which is + then refined by ParseContext.token_generator. + + >>> from pprint import * + >>> pprint(list((kind, text) for kind, text, _ in tokenize_template( + ... '%for x in range(10):\n% print x\n%end\njuicebox'))) + [('gybLines', '%for x in range(10):\n% print x'), + ('gybLinesClose', '%end'), + ('literal', 'juicebox')] + + >>> pprint(list((kind, text) for kind, text, _ in tokenize_template( + ... '''Nothing + ... % if x: + ... % for i in range(3): + ... ${i} + ... % end + ... % else: + ... THIS SHOULD NOT APPEAR IN THE OUTPUT + ... '''))) + [('literal', 'Nothing\n'), + ('gybLines', '% if x:\n% for i in range(3):'), + ('substitutionOpen', '${'), + ('literal', '\n'), + ('gybLinesClose', '% end'), + ('gybLines', '% else:'), + ('literal', 'THIS SHOULD NOT APPEAR IN THE OUTPUT\n')] + + >>> for kind, text, _ in tokenize_template(''' + ... This is $some$ literal stuff containing a ${substitution} + ... followed by a %{...} block: + ... %{ + ... # Python code + ... }% + ... and here $${are} some %-lines: + ... % x = 1 + ... % y = 2 + ... % if z == 3: + ... % print '${hello}' + ... % end + ... % for x in zz: + ... % print x + ... % # different indentation + ... % twice + ... and some lines that literally start with a %% token + ... %% first line + ... %% second line + ... '''): + ... print((kind, text.strip().split('\n',1)[0])) + ('literal', 'This is $some$ literal stuff containing a') + ('substitutionOpen', '${') + ('literal', 'followed by a %{...} block:') + ('gybBlockOpen', '%{') + ('literal', 'and here ${are} some %-lines:') + ('gybLines', '% x = 1') + ('gybLinesClose', '% end') + ('gybLines', '% for x in zz:') + ('gybLines', '% # different indentation') + ('gybLines', '% twice') + ('literal', 'and some lines that literally start with a % token') + """ + pos = 0 + end = len(template_text) + + saved_literal = [] + literal_first_match = None + + while pos < end: + m = tokenize_re.match(template_text, pos, end) + + # pull out the one matched key (ignoring internal patterns starting + # with _) + ((kind, text), ) = ( + (kind, text) for (kind, text) in m.groupdict().items() + if text is not None and kind[0] != '_') + + if kind in ('literal', 'symbol'): + if len(saved_literal) == 0: + literal_first_match = m + # literals and symbols get batched together + saved_literal.append(text) + pos = None + else: + # found a non-literal. First yield any literal we've accumulated + if saved_literal != []: + yield 'literal', ''.join(saved_literal), literal_first_match + saved_literal = [] + + # Then yield the thing we found. If we get a reply, it's + # the place to resume tokenizing + pos = yield kind, text, m + + # If we were not sent a new position by our client, resume + # tokenizing at the end of this match. + if pos is None: + pos = m.end(0) + else: + # Client is not yet ready to process next token + yield + + if saved_literal != []: + yield 'literal', ''.join(saved_literal), literal_first_match + + +def split_gyb_lines(source_lines): + r"""Return a list of lines at which to split the incoming source + + These positions represent the beginnings of python line groups that + will require a matching %end construct if they are to be closed. + + >>> src = split_lines('''\ + ... if x: + ... print x + ... if y: # trailing comment + ... print z + ... if z: # another comment\ + ... ''') + >>> s = split_gyb_lines(src) + >>> len(s) + 2 + >>> src[s[0]] + ' print z\n' + >>> s[1] - len(src) + 0 + + >>> src = split_lines('''\ + ... if x: + ... if y: print 1 + ... if z: + ... print 2 + ... pass\ + ... ''') + >>> s = split_gyb_lines(src) + >>> len(s) + 1 + >>> src[s[0]] + ' if y: print 1\n' + + >>> src = split_lines('''\ + ... if x: + ... if y: + ... print 1 + ... print 2 + ... ''') + >>> s = split_gyb_lines(src) + >>> len(s) + 2 + >>> src[s[0]] + ' if y:\n' + >>> src[s[1]] + ' print 1\n' + """ + last_token_text, last_token_kind = None, None + unmatched_indents = [] + + dedents = 0 + try: + for token_kind, token_text, token_start, \ + (token_end_line, token_end_col), line_text \ + in tokenize.generate_tokens(lambda i=iter(source_lines): + next(i)): + + if token_kind in (tokenize.COMMENT, tokenize.ENDMARKER): + continue + + if token_text == '\n' and last_token_text == ':': + unmatched_indents.append(token_end_line) + + # The tokenizer appends dedents at EOF; don't consider + # those as matching indentations. Instead just save them + # up... + if last_token_kind == tokenize.DEDENT: + dedents += 1 + # And count them later, when we see something real. + if token_kind != tokenize.DEDENT and dedents > 0: + unmatched_indents = unmatched_indents[:-dedents] + dedents = 0 + + last_token_text, last_token_kind = token_text, token_kind + + except tokenize.TokenError: + # Let the later compile() call report the error + return [] + + if last_token_text == ':': + unmatched_indents.append(len(source_lines)) + + return unmatched_indents + + +def code_starts_with_dedent_keyword(source_lines): + r"""Return True iff the incoming Python source_lines begin with "else", + "elif", "except", or "finally". + + Initial comments and whitespace are ignored. + + >>> code_starts_with_dedent_keyword(split_lines('if x in y: pass')) + False + >>> code_starts_with_dedent_keyword(split_lines('except ifSomethingElse:')) + True + >>> code_starts_with_dedent_keyword( + ... split_lines('\n# comment\nelse: # yes')) + True + """ + token_text = None + for token_kind, token_text, _, _, _ \ + in tokenize.generate_tokens(lambda i=iter(source_lines): next(i)): + + if token_kind != tokenize.COMMENT and token_text.strip() != '': + break + + return token_text in ('else', 'elif', 'except', 'finally') + + +class ParseContext(object): + + """State carried through a parse of a template""" + + filename = '' + template = '' + line_starts = [] + code_start_line = -1 + code_text = None + tokens = None # The rest of the tokens + close_lines = False + + def __init__(self, filename, template=None): + self.filename = os.path.abspath(filename) + if sys.platform == 'win32': + self.filename = self.filename.replace('\\', '/') + if template is None: + with open(filename) as f: + self.template = f.read() + else: + self.template = template + self.line_starts = get_line_starts(self.template) + self.tokens = self.token_generator(tokenize_template(self.template)) + self.next_token() + + def pos_to_line(self, pos): + return bisect(self.line_starts, pos) - 1 + + def token_generator(self, base_tokens): + r"""Given an iterator over (kind, text, match) triples (see + tokenize_template above), return a refined iterator over + token_kinds. + + Among other adjustments to the elements found by base_tokens, + this refined iterator tokenizes python code embedded in + template text to help determine its true extent. The + expression "base_tokens.send(pos)" is used to reset the index at + which base_tokens resumes scanning the underlying text. + + >>> ctx = ParseContext('dummy', ''' + ... %for x in y: + ... % print x + ... % end + ... literally + ... ''') + >>> while ctx.token_kind: + ... print((ctx.token_kind, ctx.code_text or ctx.token_text)) + ... ignored = ctx.next_token() + ('literal', '\n') + ('gybLinesOpen', 'for x in y:\n') + ('gybLines', ' print x\n') + ('gybLinesClose', '% end') + ('literal', 'literally\n') + + >>> ctx = ParseContext('dummy', + ... '''Nothing + ... % if x: + ... % for i in range(3): + ... ${i} + ... % end + ... % else: + ... THIS SHOULD NOT APPEAR IN THE OUTPUT + ... ''') + >>> while ctx.token_kind: + ... print((ctx.token_kind, ctx.code_text or ctx.token_text)) + ... ignored = ctx.next_token() + ('literal', 'Nothing\n') + ('gybLinesOpen', 'if x:\n') + ('gybLinesOpen', ' for i in range(3):\n') + ('substitutionOpen', 'i') + ('literal', '\n') + ('gybLinesClose', '% end') + ('gybLinesOpen', 'else:\n') + ('literal', 'THIS SHOULD NOT APPEAR IN THE OUTPUT\n') + + >>> ctx = ParseContext('dummy', + ... '''% for x in [1, 2, 3]: + ... % if x == 1: + ... literal1 + ... % elif x > 1: # add output line here to fix bug + ... % if x == 2: + ... literal2 + ... % end + ... % end + ... % end + ... ''') + >>> while ctx.token_kind: + ... print((ctx.token_kind, ctx.code_text or ctx.token_text)) + ... ignored = ctx.next_token() + ('gybLinesOpen', 'for x in [1, 2, 3]:\n') + ('gybLinesOpen', ' if x == 1:\n') + ('literal', 'literal1\n') + ('gybLinesOpen', 'elif x > 1: # add output line here to fix bug\n') + ('gybLinesOpen', ' if x == 2:\n') + ('literal', 'literal2\n') + ('gybLinesClose', '% end') + ('gybLinesClose', '% end') + ('gybLinesClose', '% end') + """ + for self.token_kind, self.token_text, self.token_match in base_tokens: + kind = self.token_kind + self.code_text = None + + # Do we need to close the current lines? + self.close_lines = kind == 'gybLinesClose' + + # %{...}% and ${...} constructs + if kind.endswith('Open'): + + # Tokenize text that follows as Python up to an unmatched '}' + code_start = self.token_match.end(kind) + self.code_start_line = self.pos_to_line(code_start) + + close_pos = tokenize_python_to_unmatched_close_curly( + self.template, code_start, self.line_starts) + self.code_text = self.template[code_start:close_pos] + yield kind + + if (kind == 'gybBlockOpen'): + # Absorb any '}% \n' + m2 = gyb_block_close.match(self.template, close_pos) + if not m2: + raise ValueError("Invalid block closure") + next_pos = m2.end(0) + else: + assert kind == 'substitutionOpen' + # skip past the closing '}' + next_pos = close_pos + 1 + + # Resume tokenizing after the end of the code. + base_tokens.send(next_pos) + + elif kind == 'gybLines': + + self.code_start_line = self.pos_to_line( + self.token_match.start('gybLines')) + indentation = self.token_match.group('_indent') + + # Strip off the leading indentation and %-sign + source_lines = re.split( + '^' + re.escape(indentation), + self.token_match.group('gybLines') + '\n', + flags=re.MULTILINE)[1:] + + if code_starts_with_dedent_keyword(source_lines): + self.close_lines = True + + last_split = 0 + for line in split_gyb_lines(source_lines): + self.token_kind = 'gybLinesOpen' + self.code_text = ''.join(source_lines[last_split:line]) + yield self.token_kind + last_split = line + self.code_start_line += line - last_split + self.close_lines = False + + self.code_text = ''.join(source_lines[last_split:]) + if self.code_text: + self.token_kind = 'gybLines' + yield self.token_kind + else: + yield self.token_kind + + def next_token(self): + """Move to the next token""" + for kind in self.tokens: + return self.token_kind + + self.token_kind = None + + +_default_line_directive = \ + '// ###sourceLocation(file: "%(file)s", line: %(line)d)' + + +class ExecutionContext(object): + + """State we pass around during execution of a template""" + + def __init__(self, line_directive=_default_line_directive, + **local_bindings): + self.local_bindings = local_bindings + self.line_directive = line_directive + self.local_bindings['__context__'] = self + self.result_text = [] + self.last_file_line = None + + def append_text(self, text, file, line): + # see if we need to inject a line marker + if self.line_directive: + if (file, line) != self.last_file_line: + # We can only insert the line directive at a line break + if len(self.result_text) == 0 \ + or self.result_text[-1].endswith('\n'): + substitutions = {'file': file, 'line': line + 1} + format_str = self.line_directive + '\n' + self.result_text.append(format_str % substitutions) + # But if the new text contains any line breaks, we can create + # one + elif '\n' in text: + i = text.find('\n') + self.result_text.append(text[:i + 1]) + # and try again + self.append_text(text[i + 1:], file, line + 1) + return + + self.result_text.append(text) + self.last_file_line = (file, line + text.count('\n')) + + +class ASTNode(object): + + """Abstract base class for template AST nodes""" + + def __init__(self): + raise NotImplementedError("ASTNode.__init__ is not implemented.") + + def execute(self, context): + raise NotImplementedError("ASTNode.execute is not implemented.") + + def __str__(self, indent=''): + raise NotImplementedError("ASTNode.__str__ is not implemented.") + + def format_children(self, indent): + if not self.children: + return ' []' + + return '\n'.join( + ['', indent + '['] + # noqa: W504 + [x.__str__(indent + 4 * ' ') for x in self.children] + # noqa: W504 + [indent + ']']) + + +class Block(ASTNode): + + """A sequence of other AST nodes, to be executed in order""" + + children = [] + + def __init__(self, context): + self.children = [] + + while context.token_kind and not context.close_lines: + if context.token_kind == 'literal': + node = Literal + else: + node = Code + self.children.append(node(context)) + + def execute(self, context): + for x in self.children: + x.execute(context) + + def __str__(self, indent=''): + return indent + 'Block:' + self.format_children(indent) + + +class Literal(ASTNode): + + """An AST node that generates literal text""" + + def __init__(self, context): + self.text = context.token_text + start_position = context.token_match.start(context.token_kind) + self.start_line_number = context.pos_to_line(start_position) + self.filename = context.filename + context.next_token() + + def execute(self, context): + context.append_text(self.text, self.filename, self.start_line_number) + + def __str__(self, indent=''): + return '\n'.join( + [indent + x for x in ['Literal:'] + # noqa: W504 + strip_trailing_nl(self.text).split('\n')]) + + +class Code(ASTNode): + + """An AST node that is evaluated as Python""" + + code = None + children = () + kind = None + + def __init__(self, context): + + source = '' + source_line_count = 0 + + def accumulate_code(): + s = source + (context.code_start_line - source_line_count) * '\n' \ + + textwrap.dedent(context.code_text) + line_count = context.code_start_line + \ + context.code_text.count('\n') + context.next_token() + return s, line_count + + eval_exec = 'exec' + if context.token_kind.startswith('substitution'): + eval_exec = 'eval' + source, source_line_count = accumulate_code() + source = '(' + source.strip() + ')' + + else: + while context.token_kind == 'gybLinesOpen': + source, source_line_count = accumulate_code() + source += ' __children__[%d].execute(__context__)\n' % len( + self.children) + source_line_count += 1 + + self.children += (Block(context),) + + if context.token_kind == 'gybLinesClose': + context.next_token() + + if context.token_kind == 'gybLines': + source, source_line_count = accumulate_code() + + # Only handle a substitution as part of this code block if + # we don't already have some %-lines. + elif context.token_kind == 'gybBlockOpen': + + # Opening ${...} and %{...}% constructs + source, source_line_count = accumulate_code() + + self.filename = context.filename + self.start_line_number = context.code_start_line + self.code = compile(source, context.filename, eval_exec) + self.source = source + + def execute(self, context): + # Save __children__ from the local bindings + save_children = context.local_bindings.get('__children__') + # Execute the code with our __children__ in scope + context.local_bindings['__children__'] = self.children + context.local_bindings['__file__'] = self.filename + result = eval(self.code, context.local_bindings) + + if context.local_bindings['__children__'] is not self.children: + raise ValueError("The code is not allowed to mutate __children__") + # Restore the bindings + context.local_bindings['__children__'] = save_children + + # If we got a result, the code was an expression, so append + # its value + if result is not None \ + or (isinstance(result, basestring) and result != ''): + from numbers import Number, Integral + result_string = None + if isinstance(result, Number) and not isinstance(result, Integral): + result_string = repr(result) + else: + result_string = str(result) + context.append_text( + result_string, self.filename, self.start_line_number) + + def __str__(self, indent=''): + source_lines = re.sub(r'^\n', '', strip_trailing_nl( + self.source), flags=re.MULTILINE).split('\n') + if len(source_lines) == 1: + s = indent + 'Code: {' + source_lines[0] + '}' + else: + s = indent + 'Code:\n' + indent + '{\n' + '\n'.join( + indent + 4 * ' ' + line for line in source_lines + ) + '\n' + indent + '}' + return s + self.format_children(indent) + + +def expand(filename, line_directive=_default_line_directive, **local_bindings): + r"""Return the contents of the given template file, executed with the given + local bindings. + + >>> from tempfile import NamedTemporaryFile + >>> # On Windows, the name of a NamedTemporaryFile cannot be used to open + >>> # the file for a second time if delete=True. Therefore, we have to + >>> # manually handle closing and deleting this file to allow us to open + >>> # the file by its name across all platforms. + >>> f = NamedTemporaryFile(delete=False) + >>> f.write( + ... r'''--- + ... % for i in range(int(x)): + ... a pox on ${i} for epoxy + ... % end + ... ${120 + + ... + ... 3} + ... abc + ... ${"w\nx\nX\ny"} + ... z + ... ''') + >>> f.flush() + >>> result = expand( + ... f.name, + ... line_directive='//#sourceLocation(file: "%(file)s", ' + \ + ... 'line: %(line)d)', + ... x=2 + ... ).replace( + ... '"%s"' % f.name.replace('\\', '/'), '"dummy.file"') + >>> print(result, end='') + //#sourceLocation(file: "dummy.file", line: 1) + --- + //#sourceLocation(file: "dummy.file", line: 3) + a pox on 0 for epoxy + //#sourceLocation(file: "dummy.file", line: 3) + a pox on 1 for epoxy + //#sourceLocation(file: "dummy.file", line: 5) + 123 + //#sourceLocation(file: "dummy.file", line: 8) + abc + w + x + X + y + //#sourceLocation(file: "dummy.file", line: 10) + z + >>> f.close() + >>> os.remove(f.name) + """ + with open(filename) as f: + t = parse_template(filename, f.read()) + d = os.getcwd() + os.chdir(os.path.dirname(os.path.abspath(filename))) + try: + return execute_template( + t, line_directive=line_directive, **local_bindings) + finally: + os.chdir(d) + + +def parse_template(filename, text=None): + r"""Return an AST corresponding to the given template file. + + If text is supplied, it is assumed to be the contents of the file, + as a string. + + >>> print(parse_template('dummy.file', text= + ... '''% for x in [1, 2, 3]: + ... % if x == 1: + ... literal1 + ... % elif x > 1: # add output line after this line to fix bug + ... % if x == 2: + ... literal2 + ... % end + ... % end + ... % end + ... ''')) + Block: + [ + Code: + { + for x in [1, 2, 3]: + __children__[0].execute(__context__) + } + [ + Block: + [ + Code: + { + if x == 1: + __children__[0].execute(__context__) + elif x > 1: # add output line after this line to fix bug + __children__[1].execute(__context__) + } + [ + Block: + [ + Literal: + literal1 + ] + Block: + [ + Code: + { + if x == 2: + __children__[0].execute(__context__) + } + [ + Block: + [ + Literal: + literal2 + ] + ] + ] + ] + ] + ] + ] + + >>> print(parse_template( + ... 'dummy.file', + ... text='%for x in range(10):\n% print(x)\n%end\njuicebox')) + Block: + [ + Code: + { + for x in range(10): + __children__[0].execute(__context__) + } + [ + Block: + [ + Code: {print(x)} [] + ] + ] + Literal: + juicebox + ] + + >>> print(parse_template('/dummy.file', text= + ... '''Nothing + ... % if x: + ... % for i in range(3): + ... ${i} + ... % end + ... % else: + ... THIS SHOULD NOT APPEAR IN THE OUTPUT + ... ''')) + Block: + [ + Literal: + Nothing + Code: + { + if x: + __children__[0].execute(__context__) + else: + __children__[1].execute(__context__) + } + [ + Block: + [ + Code: + { + for i in range(3): + __children__[0].execute(__context__) + } + [ + Block: + [ + Code: {(i)} [] + Literal: + + ] + ] + ] + Block: + [ + Literal: + THIS SHOULD NOT APPEAR IN THE OUTPUT + ] + ] + ] + + >>> print(parse_template('dummy.file', text='''% + ... %for x in y: + ... % print(y) + ... ''')) + Block: + [ + Code: + { + for x in y: + __children__[0].execute(__context__) + } + [ + Block: + [ + Code: {print(y)} [] + ] + ] + ] + + >>> print(parse_template('dummy.file', text='''% + ... %if x: + ... % print(y) + ... AAAA + ... %else: + ... BBBB + ... ''')) + Block: + [ + Code: + { + if x: + __children__[0].execute(__context__) + else: + __children__[1].execute(__context__) + } + [ + Block: + [ + Code: {print(y)} [] + Literal: + AAAA + ] + Block: + [ + Literal: + BBBB + ] + ] + ] + + >>> print(parse_template('dummy.file', text='''% + ... %if x: + ... % print(y) + ... AAAA + ... %# This is a comment + ... %else: + ... BBBB + ... ''')) + Block: + [ + Code: + { + if x: + __children__[0].execute(__context__) + # This is a comment + else: + __children__[1].execute(__context__) + } + [ + Block: + [ + Code: {print(y)} [] + Literal: + AAAA + ] + Block: + [ + Literal: + BBBB + ] + ] + ] + + >>> print(parse_template('dummy.file', text='''\ + ... %for x in y: + ... AAAA + ... %if x: + ... BBBB + ... %end + ... CCCC + ... ''')) + Block: + [ + Code: + { + for x in y: + __children__[0].execute(__context__) + } + [ + Block: + [ + Literal: + AAAA + Code: + { + if x: + __children__[0].execute(__context__) + } + [ + Block: + [ + Literal: + BBBB + ] + ] + Literal: + CCCC + ] + ] + ] + """ + return Block(ParseContext(filename, text)) + + +def execute_template( + ast, line_directive=_default_line_directive, **local_bindings): + r"""Return the text generated by executing the given template AST. + + Keyword arguments become local variable bindings in the execution context + + >>> root_directory = os.path.abspath('/') + >>> file_name = (root_directory + 'dummy.file').replace('\\', '/') + >>> ast = parse_template(file_name, text= + ... '''Nothing + ... % if x: + ... % for i in range(3): + ... ${i} + ... % end + ... % else: + ... THIS SHOULD NOT APPEAR IN THE OUTPUT + ... ''') + >>> out = execute_template(ast, + ... line_directive='//#sourceLocation(file: "%(file)s", line: %(line)d)', + ... x=1) + >>> out = out.replace(file_name, "DUMMY-FILE") + >>> print(out, end="") + //#sourceLocation(file: "DUMMY-FILE", line: 1) + Nothing + //#sourceLocation(file: "DUMMY-FILE", line: 4) + 0 + //#sourceLocation(file: "DUMMY-FILE", line: 4) + 1 + //#sourceLocation(file: "DUMMY-FILE", line: 4) + 2 + + >>> ast = parse_template(file_name, text= + ... '''Nothing + ... % a = [] + ... % for x in range(3): + ... % a.append(x) + ... % end + ... ${a} + ... ''') + >>> out = execute_template(ast, + ... line_directive='//#sourceLocation(file: "%(file)s", line: %(line)d)', + ... x=1) + >>> out = out.replace(file_name, "DUMMY-FILE") + >>> print(out, end="") + //#sourceLocation(file: "DUMMY-FILE", line: 1) + Nothing + //#sourceLocation(file: "DUMMY-FILE", line: 6) + [0, 1, 2] + + >>> ast = parse_template(file_name, text= + ... '''Nothing + ... % a = [] + ... % for x in range(3): + ... % a.append(x) + ... % end + ... ${a} + ... ''') + >>> out = execute_template(ast, + ... line_directive='#line %(line)d "%(file)s"', x=1) + >>> out = out.replace(file_name, "DUMMY-FILE") + >>> print(out, end="") + #line 1 "DUMMY-FILE" + Nothing + #line 6 "DUMMY-FILE" + [0, 1, 2] + """ + execution_context = ExecutionContext( + line_directive=line_directive, **local_bindings) + ast.execute(execution_context) + return ''.join(execution_context.result_text) + + +def main(): + import argparse + import sys + + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description='Generate Your Boilerplate!', epilog=''' + A GYB template consists of the following elements: + + - Literal text which is inserted directly into the output + + - %% or $$ in literal text, which insert literal '%' and '$' + symbols respectively. + + - Substitutions of the form ${}. The Python + expression is converted to a string and the result is inserted + into the output. + + - Python code delimited by %{...}%. Typically used to inject + definitions (functions, classes, variable bindings) into the + evaluation context of the template. Common indentation is + stripped, so you can add as much indentation to the beginning + of this code as you like + + - Lines beginning with optional whitespace followed by a single + '%' and Python code. %-lines allow you to nest other + constructs inside them. To close a level of nesting, use the + "%end" construct. + + - Lines beginning with optional whitespace and followed by a + single '%' and the token "end", which close open constructs in + %-lines. + + Example template: + + - Hello - + %{ + x = 42 + def succ(a): + return a+1 + }% + + I can assure you that ${x} < ${succ(x)} + + % if int(y) > 7: + % for i in range(3): + y is greater than seven! + % end + % else: + y is less than or equal to seven + % end + + - The End. - + + When run with "gyb -Dy=9", the output is + + - Hello - + + I can assure you that 42 < 43 + + y is greater than seven! + y is greater than seven! + y is greater than seven! + + - The End. - +''' + ) + parser.add_argument( + '-D', action='append', dest='defines', metavar='NAME=VALUE', + default=[], + help='''Bindings to be set in the template's execution context''') + + parser.add_argument( + 'file', type=argparse.FileType(), + help='Path to GYB template file (defaults to stdin)', nargs='?', + default=sys.stdin) + parser.add_argument( + '-o', dest='target', type=argparse.FileType('w'), + help='Output file (defaults to stdout)', default=sys.stdout) + parser.add_argument( + '--test', action='store_true', + default=False, help='Run a self-test') + parser.add_argument( + '--verbose-test', action='store_true', + default=False, help='Run a verbose self-test') + parser.add_argument( + '--dump', action='store_true', + default=False, help='Dump the parsed template to stdout') + parser.add_argument( + '--line-directive', + default=_default_line_directive, + help=''' + Line directive format string, which will be + provided 2 substitutions, `%%(line)d` and `%%(file)s`. + + Example: `#sourceLocation(file: "%%(file)s", line: %%(line)d)` + + The default works automatically with the `line-directive` tool, + which see for more information. + ''') + + args = parser.parse_args(sys.argv[1:]) + + if args.test or args.verbose_test: + import doctest + selfmod = sys.modules[__name__] + if doctest.testmod(selfmod, verbose=args.verbose_test or None).failed: + sys.exit(1) + + bindings = dict(x.split('=', 1) for x in args.defines) + ast = parse_template(args.file.name, args.file.read()) + if args.dump: + print(ast) + # Allow the template to open files and import .py files relative to its own + # directory + os.chdir(os.path.dirname(os.path.abspath(args.file.name))) + sys.path = ['.'] + sys.path + + args.target.write(execute_template(ast, args.line_directive, **bindings)) + + +if __name__ == '__main__': + main() diff --git a/Scripts/gyb/gyb_utils.py b/Scripts/gyb/gyb_utils.py new file mode 100644 index 0000000..c709480 --- /dev/null +++ b/Scripts/gyb/gyb_utils.py @@ -0,0 +1,56 @@ + +def autogenerated_warning(): + return """ +// swift-format-ignore-file +// +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# +""" + + +def upper_first(s): + return s[:1].upper() + s[1:] + + +# Each primitive type maps to a concrete ConfigReader method. +# +# - name: The display name used in doc comments (e.g. "`Bool`", "`[String]`") +# - type: The Swift type used in signatures +# - reader_method: The ConfigReader method name (e.g. "bool", "stringArray") +primitive_types = [ + {"name": "Bool", "type": "Bool", "reader_method": "bool"}, + {"name": "[Bool]", "type": "[Bool]", "reader_method": "boolArray"}, + {"name": "Float64", "type": "Float64", "reader_method": "double"}, + {"name": "[Float64]", "type": "[Float64]", "reader_method": "doubleArray"}, + {"name": "Int", "type": "Int", "reader_method": "int"}, + {"name": "[Int]", "type": "[Int]", "reader_method": "intArray"}, + {"name": "String", "type": "String", "reader_method": "string"}, + {"name": "[String]", "type": "[String]", "reader_method": "stringArray"}, + {"name": "[UInt8]", "type": "[UInt8]", "reader_method": "bytes"}, + {"name": "[[UInt8]]", "type": "[[UInt8]]", "reader_method": "byteChunkArray"}, +] + + +# String-convertible types use `reader.string(forKey:as:...)` and +# `reader.stringArray(forKey:as:...)`. +# +# - protocol: The Swift protocol constraint +# - doc_name: Display name for doc comments +string_convertible_types = [ + {"protocol": "RawRepresentable", "doc_name": "RawRepresentable"}, + {"protocol": "ExpressibleByConfigString", "doc_name": "ExpressibleByConfigString"}, +] + + +# Int-convertible types use `reader.int(forKey:as:...)` and +# `reader.intArray(forKey:as:...)`. +# +# - protocol: The Swift protocol constraint +# - doc_name: Display name for doc comments +int_convertible_types = [ + {"protocol": "RawRepresentable", "doc_name": "RawRepresentable"}, + {"protocol": "ExpressibleByConfigInt", "doc_name": "ExpressibleByConfigInt"}, +] diff --git a/Sources/DevConfiguration/Core/ConfigVariable.swift b/Sources/DevConfiguration/Core/ConfigVariable.swift index 4fcdb4c..ccf595f 100644 --- a/Sources/DevConfiguration/Core/ConfigVariable.swift +++ b/Sources/DevConfiguration/Core/ConfigVariable.swift @@ -26,6 +26,8 @@ import Configuration /// - `[Float64]` or `[Double]` /// - `[Int]` /// - `[String]` +/// - Types conforming to `RawRepresentable` or `ExpressibleByConfigInt` +/// - Types conforming to `RawRepresentable` or `ExpressibleByConfigString` @dynamicMemberLookup public struct ConfigVariable: Sendable where Value: Sendable { /// The configuration key used to look up this variable's value. diff --git a/Sources/DevConfiguration/Core/ConfigVariableReader+Functions.swift b/Sources/DevConfiguration/Core/ConfigVariableReader+Functions.swift new file mode 100644 index 0000000..59f8994 --- /dev/null +++ b/Sources/DevConfiguration/Core/ConfigVariableReader+Functions.swift @@ -0,0 +1,1627 @@ +// +// ConfigVariableReader+Functions.swift +// DevConfiguration +// +// Generated using gyb. +// + +// swift-format-ignore-file +// +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# + +import Configuration + +// MARK: - Get + +extension ConfigVariableReader { + /// Gets the value for the specified `Bool` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> Bool { + return reader.bool( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Gets the value for the specified `[Bool]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable<[Bool]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [Bool] { + return reader.boolArray( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Gets the value for the specified `Float64` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> Float64 { + return reader.double( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Gets the value for the specified `[Float64]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable<[Float64]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [Float64] { + return reader.doubleArray( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Gets the value for the specified `Int` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> Int { + return reader.int( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Gets the value for the specified `[Int]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable<[Int]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [Int] { + return reader.intArray( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Gets the value for the specified `String` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> String { + return reader.string( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Gets the value for the specified `[String]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable<[String]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [String] { + return reader.stringArray( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Gets the value for the specified `[UInt8]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable<[UInt8]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [UInt8] { + return reader.bytes( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Gets the value for the specified `[[UInt8]]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable<[[UInt8]]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [[UInt8]] { + return reader.byteChunkArray( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Gets the value for the specified `RawRepresentable` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> Value + where Value: RawRepresentable { + return reader.string( + forKey: variable.key, + as: Value.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Gets the value for the specified `[RawRepresentable]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [Element] + where Element: RawRepresentable { + return reader.stringArray( + forKey: variable.key, + as: Element.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Gets the value for the specified `ExpressibleByConfigString` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> Value + where Value: ExpressibleByConfigString { + return reader.string( + forKey: variable.key, + as: Value.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Gets the value for the specified `[ExpressibleByConfigString]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [Element] + where Element: ExpressibleByConfigString { + return reader.stringArray( + forKey: variable.key, + as: Element.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Gets the value for the specified `RawRepresentable` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> Value + where Value: RawRepresentable { + return reader.int( + forKey: variable.key, + as: Value.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Gets the value for the specified `[RawRepresentable]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [Element] + where Element: RawRepresentable { + return reader.intArray( + forKey: variable.key, + as: Element.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Gets the value for the specified `ExpressibleByConfigInt` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> Value + where Value: ExpressibleByConfigInt { + return reader.int( + forKey: variable.key, + as: Value.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Gets the value for the specified `[ExpressibleByConfigInt]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [Element] + where Element: ExpressibleByConfigInt { + return reader.intArray( + forKey: variable.key, + as: Element.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } +} + + +// MARK: - Subscript Get + +extension ConfigVariableReader { + /// Gets the value for the specified `Bool` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> Bool { + value(for: variable, fileID: fileID, line: line) + } + + + /// Gets the value for the specified `[Bool]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable<[Bool]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [Bool] { + value(for: variable, fileID: fileID, line: line) + } + + + /// Gets the value for the specified `Float64` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> Float64 { + value(for: variable, fileID: fileID, line: line) + } + + + /// Gets the value for the specified `[Float64]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable<[Float64]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [Float64] { + value(for: variable, fileID: fileID, line: line) + } + + + /// Gets the value for the specified `Int` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> Int { + value(for: variable, fileID: fileID, line: line) + } + + + /// Gets the value for the specified `[Int]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable<[Int]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [Int] { + value(for: variable, fileID: fileID, line: line) + } + + + /// Gets the value for the specified `String` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> String { + value(for: variable, fileID: fileID, line: line) + } + + + /// Gets the value for the specified `[String]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable<[String]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [String] { + value(for: variable, fileID: fileID, line: line) + } + + + /// Gets the value for the specified `[UInt8]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable<[UInt8]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [UInt8] { + value(for: variable, fileID: fileID, line: line) + } + + + /// Gets the value for the specified `[[UInt8]]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable<[[UInt8]]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [[UInt8]] { + value(for: variable, fileID: fileID, line: line) + } + + + /// Gets the value for the specified `RawRepresentable` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> Value + where Value: RawRepresentable { + value(for: variable, fileID: fileID, line: line) + } + + + /// Gets the value for the specified `[RawRepresentable]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [Element] + where Element: RawRepresentable { + value(for: variable, fileID: fileID, line: line) + } + + + /// Gets the value for the specified `ExpressibleByConfigString` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> Value + where Value: ExpressibleByConfigString { + value(for: variable, fileID: fileID, line: line) + } + + + /// Gets the value for the specified `[ExpressibleByConfigString]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [Element] + where Element: ExpressibleByConfigString { + value(for: variable, fileID: fileID, line: line) + } + + + /// Gets the value for the specified `RawRepresentable` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> Value + where Value: RawRepresentable { + value(for: variable, fileID: fileID, line: line) + } + + + /// Gets the value for the specified `[RawRepresentable]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [Element] + where Element: RawRepresentable { + value(for: variable, fileID: fileID, line: line) + } + + + /// Gets the value for the specified `ExpressibleByConfigInt` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> Value + where Value: ExpressibleByConfigInt { + value(for: variable, fileID: fileID, line: line) + } + + + /// Gets the value for the specified `[ExpressibleByConfigInt]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [Element] + where Element: ExpressibleByConfigInt { + value(for: variable, fileID: fileID, line: line) + } +} + + +// MARK: - Fetch + +extension ConfigVariableReader { + /// Asynchronously fetches the value for the specified `Bool` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> Bool { + return try await reader.fetchBool( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Asynchronously fetches the value for the specified `[Bool]` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable<[Bool]>, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> [Bool] { + return try await reader.fetchBoolArray( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Asynchronously fetches the value for the specified `Float64` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> Float64 { + return try await reader.fetchDouble( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Asynchronously fetches the value for the specified `[Float64]` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable<[Float64]>, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> [Float64] { + return try await reader.fetchDoubleArray( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Asynchronously fetches the value for the specified `Int` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> Int { + return try await reader.fetchInt( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Asynchronously fetches the value for the specified `[Int]` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable<[Int]>, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> [Int] { + return try await reader.fetchIntArray( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Asynchronously fetches the value for the specified `String` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> String { + return try await reader.fetchString( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Asynchronously fetches the value for the specified `[String]` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable<[String]>, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> [String] { + return try await reader.fetchStringArray( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Asynchronously fetches the value for the specified `[UInt8]` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable<[UInt8]>, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> [UInt8] { + return try await reader.fetchBytes( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Asynchronously fetches the value for the specified `[[UInt8]]` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable<[[UInt8]]>, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> [[UInt8]] { + return try await reader.fetchByteChunkArray( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Asynchronously fetches the value for the specified `RawRepresentable` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> Value + where Value: RawRepresentable { + return try await reader.fetchString( + forKey: variable.key, + as: Value.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Asynchronously fetches the value for the specified `[RawRepresentable]` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> [Element] + where Element: RawRepresentable { + return try await reader.fetchStringArray( + forKey: variable.key, + as: Element.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Asynchronously fetches the value for the specified `ExpressibleByConfigString` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> Value + where Value: ExpressibleByConfigString { + return try await reader.fetchString( + forKey: variable.key, + as: Value.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Asynchronously fetches the value for the specified `[ExpressibleByConfigString]` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> [Element] + where Element: ExpressibleByConfigString { + return try await reader.fetchStringArray( + forKey: variable.key, + as: Element.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Asynchronously fetches the value for the specified `RawRepresentable` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> Value + where Value: RawRepresentable { + return try await reader.fetchInt( + forKey: variable.key, + as: Value.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Asynchronously fetches the value for the specified `[RawRepresentable]` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> [Element] + where Element: RawRepresentable { + return try await reader.fetchIntArray( + forKey: variable.key, + as: Element.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Asynchronously fetches the value for the specified `ExpressibleByConfigInt` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> Value + where Value: ExpressibleByConfigInt { + return try await reader.fetchInt( + forKey: variable.key, + as: Value.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Asynchronously fetches the value for the specified `[ExpressibleByConfigInt]` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> [Element] + where Element: ExpressibleByConfigInt { + return try await reader.fetchIntArray( + forKey: variable.key, + as: Element.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } +} + + +// MARK: - Watch + +extension ConfigVariableReader { + /// Watches for updates to the value for the specified `Bool` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence) async throws -> Return + ) async throws -> Return + where Return: ~Copyable { + try await reader.watchBool( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + + + /// Watches for updates to the value for the specified `[Bool]` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable<[Bool]>, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<[Bool], Never>) async throws -> Return + ) async throws -> Return + where Return: ~Copyable { + try await reader.watchBoolArray( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + + + /// Watches for updates to the value for the specified `Float64` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence) async throws -> Return + ) async throws -> Return + where Return: ~Copyable { + try await reader.watchDouble( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + + + /// Watches for updates to the value for the specified `[Float64]` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable<[Float64]>, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<[Float64], Never>) async throws -> Return + ) async throws -> Return + where Return: ~Copyable { + try await reader.watchDoubleArray( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + + + /// Watches for updates to the value for the specified `Int` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence) async throws -> Return + ) async throws -> Return + where Return: ~Copyable { + try await reader.watchInt( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + + + /// Watches for updates to the value for the specified `[Int]` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable<[Int]>, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<[Int], Never>) async throws -> Return + ) async throws -> Return + where Return: ~Copyable { + try await reader.watchIntArray( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + + + /// Watches for updates to the value for the specified `String` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence) async throws -> Return + ) async throws -> Return + where Return: ~Copyable { + try await reader.watchString( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + + + /// Watches for updates to the value for the specified `[String]` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable<[String]>, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<[String], Never>) async throws -> Return + ) async throws -> Return + where Return: ~Copyable { + try await reader.watchStringArray( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + + + /// Watches for updates to the value for the specified `[UInt8]` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable<[UInt8]>, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<[UInt8], Never>) async throws -> Return + ) async throws -> Return + where Return: ~Copyable { + try await reader.watchBytes( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + + + /// Watches for updates to the value for the specified `[[UInt8]]` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable<[[UInt8]]>, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<[[UInt8]], Never>) async throws -> Return + ) async throws -> Return + where Return: ~Copyable { + try await reader.watchByteChunkArray( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + + + /// Watches for updates to the value for the specified `RawRepresentable` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence) async throws -> Return + ) async throws -> Return + where Value: RawRepresentable & Sendable { + try await reader.watchString( + forKey: variable.key, + as: Value.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + + + /// Watches for updates to the value for the specified `[RawRepresentable]` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<[Element], Never>) async throws -> Return + ) async throws -> Return + where Element: RawRepresentable & Sendable { + try await reader.watchStringArray( + forKey: variable.key, + as: Element.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + + + /// Watches for updates to the value for the specified `ExpressibleByConfigString` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence) async throws -> Return + ) async throws -> Return + where Value: ExpressibleByConfigString & Sendable { + try await reader.watchString( + forKey: variable.key, + as: Value.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + + + /// Watches for updates to the value for the specified `[ExpressibleByConfigString]` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<[Element], Never>) async throws -> Return + ) async throws -> Return + where Element: ExpressibleByConfigString & Sendable { + try await reader.watchStringArray( + forKey: variable.key, + as: Element.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + + + /// Watches for updates to the value for the specified `RawRepresentable` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence) async throws -> Return + ) async throws -> Return + where Value: RawRepresentable & Sendable { + try await reader.watchInt( + forKey: variable.key, + as: Value.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + + + /// Watches for updates to the value for the specified `[RawRepresentable]` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<[Element], Never>) async throws -> Return + ) async throws -> Return + where Element: RawRepresentable & Sendable { + try await reader.watchIntArray( + forKey: variable.key, + as: Element.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + + + /// Watches for updates to the value for the specified `ExpressibleByConfigInt` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence) async throws -> Return + ) async throws -> Return + where Value: ExpressibleByConfigInt & Sendable { + try await reader.watchInt( + forKey: variable.key, + as: Value.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + + + /// Watches for updates to the value for the specified `[ExpressibleByConfigInt]` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<[Element], Never>) async throws -> Return + ) async throws -> Return + where Element: ExpressibleByConfigInt & Sendable { + try await reader.watchIntArray( + forKey: variable.key, + as: Element.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } +} diff --git a/Sources/DevConfiguration/Core/ConfigVariableReader+Functions.swift.gyb b/Sources/DevConfiguration/Core/ConfigVariableReader+Functions.swift.gyb new file mode 100644 index 0000000..149f1ef --- /dev/null +++ b/Sources/DevConfiguration/Core/ConfigVariableReader+Functions.swift.gyb @@ -0,0 +1,559 @@ +%{ +import sys +import os + +# Add Scripts/gyb directory to path so we can import gyb_utils +scripts_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', '..', 'Scripts', 'gyb') +sys.path.insert(0, scripts_dir) + +from gyb_utils import * +}% +// +// ConfigVariableReader+Functions.swift +// DevConfiguration +// +// Generated using gyb. +// +${autogenerated_warning()} +import Configuration + +// MARK: - Get + +extension ConfigVariableReader { + % for i, p in enumerate(primitive_types): + % if i > 0: + + + % end + /// Gets the value for the specified `${p["name"]}` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable<${p["type"]}>, + fileID: String = #fileID, + line: UInt = #line + ) -> ${p["type"]} { + return reader.${p["reader_method"]}( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + % end + + + % for i, ct in enumerate(string_convertible_types): + % if i > 0: + + + % end + /// Gets the value for the specified `${ct["doc_name"]}` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> Value + where Value: ${ct["protocol"]} { + return reader.string( + forKey: variable.key, + as: Value.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Gets the value for the specified `[${ct["doc_name"]}]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [Element] + where Element: ${ct["protocol"]} { + return reader.stringArray( + forKey: variable.key, + as: Element.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + % end + + + % for i, ct in enumerate(int_convertible_types): + % if i > 0: + + + % end + /// Gets the value for the specified `${ct["doc_name"]}` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> Value + where Value: ${ct["protocol"]} { + return reader.int( + forKey: variable.key, + as: Value.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Gets the value for the specified `[${ct["doc_name"]}]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func value( + for variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [Element] + where Element: ${ct["protocol"]} { + return reader.intArray( + forKey: variable.key, + as: Element.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + % end +} + + +// MARK: - Subscript Get + +extension ConfigVariableReader { + % for i, p in enumerate(primitive_types): + % if i > 0: + + + % end + /// Gets the value for the specified `${p["name"]}` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable<${p["type"]}>, + fileID: String = #fileID, + line: UInt = #line + ) -> ${p["type"]} { + value(for: variable, fileID: fileID, line: line) + } + % end + + + % for i, ct in enumerate(string_convertible_types): + % if i > 0: + + + % end + /// Gets the value for the specified `${ct["doc_name"]}` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> Value + where Value: ${ct["protocol"]} { + value(for: variable, fileID: fileID, line: line) + } + + + /// Gets the value for the specified `[${ct["doc_name"]}]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [Element] + where Element: ${ct["protocol"]} { + value(for: variable, fileID: fileID, line: line) + } + % end + + + % for i, ct in enumerate(int_convertible_types): + % if i > 0: + + + % end + /// Gets the value for the specified `${ct["doc_name"]}` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) -> Value + where Value: ${ct["protocol"]} { + value(for: variable, fileID: fileID, line: line) + } + + + /// Gets the value for the specified `[${ct["doc_name"]}]` config variable. + /// + /// - Parameters: + /// - variable: The variable to get a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public subscript( + variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line + ) -> [Element] + where Element: ${ct["protocol"]} { + value(for: variable, fileID: fileID, line: line) + } + % end +} + + +// MARK: - Fetch + +extension ConfigVariableReader { + % for i, p in enumerate(primitive_types): + % if i > 0: + + + % end + /// Asynchronously fetches the value for the specified `${p["name"]}` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable<${p["type"]}>, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> ${p["type"]} { + return try await reader.fetch${upper_first(p["reader_method"])}( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + % end + + + % for i, ct in enumerate(string_convertible_types): + % if i > 0: + + + % end + /// Asynchronously fetches the value for the specified `${ct["doc_name"]}` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> Value + where Value: ${ct["protocol"]} { + return try await reader.fetchString( + forKey: variable.key, + as: Value.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Asynchronously fetches the value for the specified `[${ct["doc_name"]}]` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> [Element] + where Element: ${ct["protocol"]} { + return try await reader.fetchStringArray( + forKey: variable.key, + as: Element.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + % end + + + % for i, ct in enumerate(int_convertible_types): + % if i > 0: + + + % end + /// Asynchronously fetches the value for the specified `${ct["doc_name"]}` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> Value + where Value: ${ct["protocol"]} { + return try await reader.fetchInt( + forKey: variable.key, + as: Value.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + + + /// Asynchronously fetches the value for the specified `[${ct["doc_name"]}]` config variable. + /// + /// - Parameters: + /// - variable: The variable to fetch a value for. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - Returns: The configuration value of the variable, or the default value if resolution fails. + public func fetchValue( + for variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line + ) async throws -> [Element] + where Element: ${ct["protocol"]} { + return try await reader.fetchIntArray( + forKey: variable.key, + as: Element.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line + ) + } + % end +} + + +// MARK: - Watch + +extension ConfigVariableReader { + % for i, p in enumerate(primitive_types): + % if i > 0: + + + % end + /// Watches for updates to the value for the specified `${p["name"]}` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable<${p["type"]}>, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<${p["type"]}, Never>) async throws -> Return + ) async throws -> Return + where Return: ~Copyable { + try await reader.watch${upper_first(p["reader_method"])}( + forKey: variable.key, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + % end + + + % for i, ct in enumerate(string_convertible_types): + % if i > 0: + + + % end + /// Watches for updates to the value for the specified `${ct["doc_name"]}` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence) async throws -> Return + ) async throws -> Return + where Value: ${ct["protocol"]} & Sendable { + try await reader.watchString( + forKey: variable.key, + as: Value.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + + + /// Watches for updates to the value for the specified `[${ct["doc_name"]}]` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<[Element], Never>) async throws -> Return + ) async throws -> Return + where Element: ${ct["protocol"]} & Sendable { + try await reader.watchStringArray( + forKey: variable.key, + as: Element.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + % end + + + % for i, ct in enumerate(int_convertible_types): + % if i > 0: + + + % end + /// Watches for updates to the value for the specified `${ct["doc_name"]}` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence) async throws -> Return + ) async throws -> Return + where Value: ${ct["protocol"]} & Sendable { + try await reader.watchInt( + forKey: variable.key, + as: Value.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + + + /// Watches for updates to the value for the specified `[${ct["doc_name"]}]` config variable. + /// + /// - Parameters: + /// - variable: The variable to watch for updates. + /// - fileID: The source file identifier for access reporting. + /// - line: The source line number for access reporting. + /// - updatesHandler: A closure that handles an async sequence of updates to the value. + /// - Returns: The result produced by the handler. + public func watchValue( + for variable: ConfigVariable<[Element]>, + fileID: String = #fileID, + line: UInt = #line, + updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<[Element], Never>) async throws -> Return + ) async throws -> Return + where Element: ${ct["protocol"]} & Sendable { + try await reader.watchIntArray( + forKey: variable.key, + as: Element.self, + isSecret: isSecret(variable), + default: variable.defaultValue, + fileID: fileID, + line: line, + updatesHandler: updatesHandler + ) + } + % end +} diff --git a/Sources/DevConfiguration/Core/ConfigVariableReader.swift b/Sources/DevConfiguration/Core/ConfigVariableReader.swift index 57887db..8dae750 100644 --- a/Sources/DevConfiguration/Core/ConfigVariableReader.swift +++ b/Sources/DevConfiguration/Core/ConfigVariableReader.swift @@ -8,7 +8,7 @@ import Configuration import DevFoundation -/// Provides structured access to configuration values queried by a `ConfigVariable`. +/// Provides access to configuration values queried by a `ConfigVariable`. /// /// A config variable reader is a type-safe wrapper around swift-configuration's `ConfigReader`. It uses /// `ConfigVariable` instances to provide compile-time type safety and structured access to configuration values. @@ -81,867 +81,61 @@ public struct ConfigVariableReader { } -// MARK: - Get - -extension ConfigVariableReader { - /// Gets the value for the specified `Bool` config variable. - /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func value( - for variable: ConfigVariable, - fileID: String = #fileID, - line: UInt = #line - ) -> Bool { - return reader.bool( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } - - - /// Gets the value for the specified `[Bool]` config variable. - /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func value( - for variable: ConfigVariable<[Bool]>, - fileID: String = #fileID, - line: UInt = #line - ) -> [Bool] { - return reader.boolArray( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } - - - /// Gets the value for the specified `Float64` config variable. - /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func value( - for variable: ConfigVariable, - fileID: String = #fileID, - line: UInt = #line - ) -> Float64 { - return reader.double( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } - - - /// Gets the value for the specified `[Float64]` config variable. - /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func value( - for variable: ConfigVariable<[Float64]>, - fileID: String = #fileID, - line: UInt = #line - ) -> [Float64] { - return reader.doubleArray( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } - - - /// Gets the value for the specified `Int` config variable. - /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func value( - for variable: ConfigVariable, - fileID: String = #fileID, - line: UInt = #line - ) -> Int { - return reader.int( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } - - - /// Gets the value for the specified `[Int]` config variable. - /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func value( - for variable: ConfigVariable<[Int]>, - fileID: String = #fileID, - line: UInt = #line - ) -> [Int] { - return reader.intArray( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } - - - /// Gets the value for the specified `String` config variable. - /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func value( - for variable: ConfigVariable, - fileID: String = #fileID, - line: UInt = #line - ) -> String { - return reader.string( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } - - - /// Gets the value for the specified `[String]` config variable. - /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func value( - for variable: ConfigVariable<[String]>, - fileID: String = #fileID, - line: UInt = #line - ) -> [String] { - return reader.stringArray( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } - - - /// Gets the value for the specified `[UInt8]` config variable. - /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func value( - for variable: ConfigVariable<[UInt8]>, - fileID: String = #fileID, - line: UInt = #line - ) -> [UInt8] { - return reader.bytes( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } - - - /// Gets the value for the specified `[[UInt8]]` config variable. - /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func value( - for variable: ConfigVariable<[[UInt8]]>, - fileID: String = #fileID, - line: UInt = #line - ) -> [[UInt8]] { - return reader.byteChunkArray( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } -} - - -// MARK: - Subscript Get - extension ConfigVariableReader { - /// Gets the value for the specified `Bool` config variable. + /// Whether the given variable is secret. /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public subscript( - variable: ConfigVariable, - fileID: String = #fileID, - line: UInt = #line - ) -> Bool { - value(for: variable, fileID: fileID, line: line) - } - - - /// Gets the value for the specified `[Bool]` config variable. + /// The default implementation returns `true` only when the secrecy is `.secret`. /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public subscript( - variable: ConfigVariable<[Bool]>, - fileID: String = #fileID, - line: UInt = #line - ) -> [Bool] { - value(for: variable, fileID: fileID, line: line) + /// - Parameter variable: The config variable whose secrecy is being determined.. + func isSecret(_ variable: ConfigVariable) -> Bool { + return variable.secrecy == .secret } - /// Gets the value for the specified `Float64` config variable. + /// Whether the given `String` variable is secret, that is, not `.public`. /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public subscript( - variable: ConfigVariable, - fileID: String = #fileID, - line: UInt = #line - ) -> Float64 { - value(for: variable, fileID: fileID, line: line) + /// - Parameter variable: The config variable whose secrecy is being determined.. + func isSecret(_ variable: ConfigVariable) -> Bool { + return variable.secrecy != .public } - /// Gets the value for the specified `[Float64]` config variable. + /// Whether the given `[String]` variable is secret, that is, not `.public`. /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public subscript( - variable: ConfigVariable<[Float64]>, - fileID: String = #fileID, - line: UInt = #line - ) -> [Float64] { - value(for: variable, fileID: fileID, line: line) + /// - Parameter variable: The config variable whose secrecy is being determined.. + func isSecret(_ variable: ConfigVariable<[String]>) -> Bool { + return variable.secrecy != .public } - /// Gets the value for the specified `Int` config variable. + /// Whether the given `RawRepresentable` variable is secret, that is, not `.public`. /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public subscript( - variable: ConfigVariable, - fileID: String = #fileID, - line: UInt = #line - ) -> Int { - value(for: variable, fileID: fileID, line: line) + /// - Parameter variable: The config variable whose secrecy is being determined.. + func isSecret(_ variable: ConfigVariable) -> Bool where Value: RawRepresentable { + return variable.secrecy != .public } - /// Gets the value for the specified `[Int]` config variable. + /// Whether the given `[RawRepresentable]` variable is secret, that is, not `.public`. /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public subscript( - variable: ConfigVariable<[Int]>, - fileID: String = #fileID, - line: UInt = #line - ) -> [Int] { - value(for: variable, fileID: fileID, line: line) + /// - Parameter variable: The config variable whose secrecy is being determined.. + func isSecret(_ variable: ConfigVariable<[Element]>) -> Bool where Element: RawRepresentable { + return variable.secrecy != .public } - /// Gets the value for the specified `String` config variable. + /// Whether the given `ExpressibleByConfigString` variable is secret, that is, not `.public`. /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public subscript( - variable: ConfigVariable, - fileID: String = #fileID, - line: UInt = #line - ) -> String { - value(for: variable, fileID: fileID, line: line) + /// - Parameter variable: The config variable whose secrecy is being determined.. + func isSecret(_ variable: ConfigVariable) -> Bool where Value: ExpressibleByConfigString { + return variable.secrecy != .public } - /// Gets the value for the specified `[String]` config variable. + /// Whether the given `[ExpressibleByConfigString]` variable is secret, that is, not `.public`. /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public subscript( - variable: ConfigVariable<[String]>, - fileID: String = #fileID, - line: UInt = #line - ) -> [String] { - value(for: variable, fileID: fileID, line: line) - } - - - /// Gets the value for the specified `[UInt8]` config variable. - /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public subscript( - variable: ConfigVariable<[UInt8]>, - fileID: String = #fileID, - line: UInt = #line - ) -> [UInt8] { - value(for: variable, fileID: fileID, line: line) - } - - - /// Gets the value for the specified `[[UInt8]]` config variable. - /// - /// - Parameters: - /// - variable: The variable to get a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public subscript( - variable: ConfigVariable<[[UInt8]]>, - fileID: String = #fileID, - line: UInt = #line - ) -> [[UInt8]] { - value(for: variable, fileID: fileID, line: line) - } -} - - -// MARK: - Fetch - -extension ConfigVariableReader { - /// Asynchronously fetches the value for the specified `Bool` config variable. - /// - /// - Parameters: - /// - variable: The variable to fetch a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func fetchValue( - for variable: ConfigVariable, - fileID: String = #fileID, - line: UInt = #line - ) async throws -> Bool { - return try await reader.fetchBool( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } - - - /// Asynchronously fetches the value for the specified `[Bool]` config variable. - /// - /// - Parameters: - /// - variable: The variable to fetch a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func fetchValue( - for variable: ConfigVariable<[Bool]>, - fileID: String = #fileID, - line: UInt = #line - ) async throws -> [Bool] { - return try await reader.fetchBoolArray( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } - - - /// Asynchronously fetches the value for the specified `Float64` config variable. - /// - /// - Parameters: - /// - variable: The variable to fetch a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func fetchValue( - for variable: ConfigVariable, - fileID: String = #fileID, - line: UInt = #line - ) async throws -> Float64 { - return try await reader.fetchDouble( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } - - - /// Asynchronously fetches the value for the specified `[Float64]` config variable. - /// - /// - Parameters: - /// - variable: The variable to fetch a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func fetchValue( - for variable: ConfigVariable<[Float64]>, - fileID: String = #fileID, - line: UInt = #line - ) async throws -> [Float64] { - return try await reader.fetchDoubleArray( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } - - - /// Asynchronously fetches the value for the specified `Int` config variable. - /// - /// - Parameters: - /// - variable: The variable to fetch a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func fetchValue( - for variable: ConfigVariable, - fileID: String = #fileID, - line: UInt = #line - ) async throws -> Int { - return try await reader.fetchInt( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } - - - /// Asynchronously fetches the value for the specified `[Int]` config variable. - /// - /// - Parameters: - /// - variable: The variable to fetch a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func fetchValue( - for variable: ConfigVariable<[Int]>, - fileID: String = #fileID, - line: UInt = #line - ) async throws -> [Int] { - return try await reader.fetchIntArray( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } - - - /// Asynchronously fetches the value for the specified `String` config variable. - /// - /// - Parameters: - /// - variable: The variable to fetch a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func fetchValue( - for variable: ConfigVariable, - fileID: String = #fileID, - line: UInt = #line - ) async throws -> String { - return try await reader.fetchString( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } - - - /// Asynchronously fetches the value for the specified `[String]` config variable. - /// - /// - Parameters: - /// - variable: The variable to fetch a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func fetchValue( - for variable: ConfigVariable<[String]>, - fileID: String = #fileID, - line: UInt = #line - ) async throws -> [String] { - return try await reader.fetchStringArray( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } - - - /// Asynchronously fetches the value for the specified `[UInt8]` config variable. - /// - /// - Parameters: - /// - variable: The variable to fetch a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func fetchValue( - for variable: ConfigVariable<[UInt8]>, - fileID: String = #fileID, - line: UInt = #line - ) async throws -> [UInt8] { - return try await reader.fetchBytes( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } - - - /// Asynchronously fetches the value for the specified `[[UInt8]]` config variable. - /// - /// - Parameters: - /// - variable: The variable to fetch a value for. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - Returns: The configuration value of the variable, or the default value if resolution fails. - public func fetchValue( - for variable: ConfigVariable<[[UInt8]]>, - fileID: String = #fileID, - line: UInt = #line - ) async throws -> [[UInt8]] { - return try await reader.fetchByteChunkArray( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line - ) - } -} - - -// MARK: - Watch - -extension ConfigVariableReader { - /// Watches for updates to the value for the specified `Bool` config variable. - /// - /// - Parameters: - /// - variable: The variable to watch for updates. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - updatesHandler: A closure that handles an async sequence of updates to the value. - /// - Returns: The result produced by the handler. - public func watchValue( - for variable: ConfigVariable, - fileID: String = #fileID, - line: UInt = #line, - updatesHandler: (_ updates: ConfigUpdatesAsyncSequence) async throws -> Return - ) async throws -> Return { - try await reader.watchBool( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line, - updatesHandler: updatesHandler - ) - } - - - /// Watches for updates to the value for the specified `[Bool]` config variable. - /// - /// - Parameters: - /// - variable: The variable to watch for updates. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - updatesHandler: A closure that handles an async sequence of updates to the value. - /// - Returns: The result produced by the handler. - public func watchValue( - for variable: ConfigVariable<[Bool]>, - fileID: String = #fileID, - line: UInt = #line, - updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<[Bool], Never>) async throws -> Return - ) async throws -> Return { - try await reader.watchBoolArray( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line, - updatesHandler: updatesHandler - ) - } - - - /// Watches for updates to the value for the specified `Float64` config variable. - /// - /// - Parameters: - /// - variable: The variable to watch for updates. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - updatesHandler: A closure that handles an async sequence of updates to the value. - /// - Returns: The result produced by the handler. - public func watchValue( - for variable: ConfigVariable, - fileID: String = #fileID, - line: UInt = #line, - updatesHandler: (_ updates: ConfigUpdatesAsyncSequence) async throws -> Return - ) async throws -> Return { - try await reader.watchDouble( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line, - updatesHandler: updatesHandler - ) - } - - - /// Watches for updates to the value for the specified `[Float64]` config variable. - /// - /// - Parameters: - /// - variable: The variable to watch for updates. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - updatesHandler: A closure that handles an async sequence of updates to the value. - /// - Returns: The result produced by the handler. - public func watchValue( - for variable: ConfigVariable<[Float64]>, - fileID: String = #fileID, - line: UInt = #line, - updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<[Float64], Never>) async throws -> Return - ) async throws -> Return { - try await reader.watchDoubleArray( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line, - updatesHandler: updatesHandler - ) - } - - - /// Watches for updates to the value for the specified `Int` config variable. - /// - /// - Parameters: - /// - variable: The variable to watch for updates. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - updatesHandler: A closure that handles an async sequence of updates to the value. - /// - Returns: The result produced by the handler. - public func watchValue( - for variable: ConfigVariable, - fileID: String = #fileID, - line: UInt = #line, - updatesHandler: (_ updates: ConfigUpdatesAsyncSequence) async throws -> Return - ) async throws -> Return { - try await reader.watchInt( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line, - updatesHandler: updatesHandler - ) - } - - - /// Watches for updates to the value for the specified `[Int]` config variable. - /// - /// - Parameters: - /// - variable: The variable to watch for updates. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - updatesHandler: A closure that handles an async sequence of updates to the value. - /// - Returns: The result produced by the handler. - public func watchValue( - for variable: ConfigVariable<[Int]>, - fileID: String = #fileID, - line: UInt = #line, - updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<[Int], Never>) async throws -> Return - ) async throws -> Return { - try await reader.watchIntArray( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line, - updatesHandler: updatesHandler - ) - } - - - /// Watches for updates to the value for the specified `String` config variable. - /// - /// - Parameters: - /// - variable: The variable to watch for updates. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - updatesHandler: A closure that handles an async sequence of updates to the value. - /// - Returns: The result produced by the handler. - public func watchValue( - for variable: ConfigVariable, - fileID: String = #fileID, - line: UInt = #line, - updatesHandler: (_ updates: ConfigUpdatesAsyncSequence) async throws -> Return - ) async throws -> Return { - try await reader.watchString( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line, - updatesHandler: updatesHandler - ) - } - - - /// Watches for updates to the value for the specified `[String]` config variable. - /// - /// - Parameters: - /// - variable: The variable to watch for updates. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - updatesHandler: A closure that handles an async sequence of updates to the value. - /// - Returns: The result produced by the handler. - public func watchValue( - for variable: ConfigVariable<[String]>, - fileID: String = #fileID, - line: UInt = #line, - updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<[String], Never>) async throws -> Return - ) async throws -> Return { - try await reader.watchStringArray( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line, - updatesHandler: updatesHandler - ) - } - - - /// Watches for updates to the value for the specified `[UInt8]` config variable. - /// - /// - Parameters: - /// - variable: The variable to watch for updates. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - updatesHandler: A closure that handles an async sequence of updates to the value. - /// - Returns: The result produced by the handler. - public func watchValue( - for variable: ConfigVariable<[UInt8]>, - fileID: String = #fileID, - line: UInt = #line, - updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<[UInt8], Never>) async throws -> Return - ) async throws -> Return { - try await reader.watchBytes( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line, - updatesHandler: updatesHandler - ) - } - - - /// Watches for updates to the value for the specified `[[UInt8]]` config variable. - /// - /// - Parameters: - /// - variable: The variable to watch for updates. - /// - fileID: The source file identifier for access reporting. - /// - line: The source line number for access reporting. - /// - updatesHandler: A closure that handles an async sequence of updates to the value. - /// - Returns: The result produced by the handler. - public func watchValue( - for variable: ConfigVariable<[[UInt8]]>, - fileID: String = #fileID, - line: UInt = #line, - updatesHandler: (_ updates: ConfigUpdatesAsyncSequence<[[UInt8]], Never>) async throws -> Return - ) async throws -> Return { - try await reader.watchByteChunkArray( - forKey: variable.key, - isSecret: variable.isSecret, - default: variable.defaultValue, - fileID: fileID, - line: line, - updatesHandler: updatesHandler - ) + /// - Parameter variable: The config variable whose secrecy is being determined.. + func isSecret(_ variable: ConfigVariable<[Element]>) -> Bool where Element: ExpressibleByConfigString { + return variable.secrecy != .public } } diff --git a/Sources/DevConfiguration/Core/ConfigVariableSecrecy.swift b/Sources/DevConfiguration/Core/ConfigVariableSecrecy.swift index d4da697..25e62b2 100644 --- a/Sources/DevConfiguration/Core/ConfigVariableSecrecy.swift +++ b/Sources/DevConfiguration/Core/ConfigVariableSecrecy.swift @@ -5,12 +5,14 @@ // Created by Duncan Lewis on 1/7/2026. // +import Configuration + /// Controls whether a configuration variable's value is treated as secret. /// /// Variable secrecy determines how values are handled in telemetry, logging, and other observability systems. Secret /// values are redacted or obfuscated to prevent sensitive information from being exposed. public enum ConfigVariableSecrecy: CaseIterable, Sendable { - /// Treats `String` and `[String]` values as secret and all other types as public. + /// Treats `String`, `[String]`, and `String`-backed values as secret and all other types as public. /// /// This is the default secrecy level and provides sensible protection for most use cases. case auto @@ -22,31 +24,7 @@ public enum ConfigVariableSecrecy: CaseIterable, Sendable { /// Never treat the value as secret. /// - /// Use this when you explicitly want values to be visible in logs and telemetry, even if they are strings or - /// string arrays. + /// Use this when you explicitly want values to be visible in logs and telemetry, even if they are strings, + /// string arrays, or string-backed. case `public` } - - -extension ConfigVariable { - /// Whether the variable is secret. - var isSecret: Bool { - return secrecy == .secret - } -} - - -extension ConfigVariable { - /// Whether the variable is secret, that is, not `.public`. - var isSecret: Bool { - return secrecy != .public - } -} - - -extension ConfigVariable<[String]> { - /// Whether the variable is secret, that is, not `.public`. - var isSecret: Bool { - return secrecy != .public - } -} diff --git a/Sources/DevConfiguration/Documentation.docc/Documentation.md b/Sources/DevConfiguration/Documentation.docc/Documentation.md index b06e8d8..d3e4092 100644 --- a/Sources/DevConfiguration/Documentation.docc/Documentation.md +++ b/Sources/DevConfiguration/Documentation.docc/Documentation.md @@ -27,7 +27,3 @@ configuration management with extensible metadata, a variable management UI, and - ``EventBusAccessReporter`` - ``ConfigVariableAccessSucceededEvent`` - ``ConfigVariableAccessFailedEvent`` - -### Supporting Types - -- ``ConfigValueReadable`` diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariable+SecrecyTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariable+SecrecyTests.swift deleted file mode 100644 index 83f7272..0000000 --- a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariable+SecrecyTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// ConfigVariable+SecrecyTests.swift -// DevConfiguration -// -// Created by Prachi Gauriar on 2/16/26. -// - -import Configuration -import DevTesting -import Testing - -@testable import DevConfiguration - -struct ConfigVariable_SecrecyTests: RandomValueGenerating { - var randomNumberGenerator = makeRandomNumberGenerator() - - - // MARK: - isSecret - - @Test(arguments: ConfigVariableSecrecy.allCases) - mutating func isSecret(secrecy: ConfigVariableSecrecy) { - let intVariable = ConfigVariable( - key: randomConfigKey(), - defaultValue: randomInt(in: .min ... .max), - secrecy: secrecy - ) - - let stringVariable = ConfigVariable( - key: randomConfigKey(), - defaultValue: randomAlphanumericString(), - secrecy: secrecy - ) - - let stringArrayVariable = ConfigVariable( - key: randomConfigKey(), - defaultValue: Array(count: randomInt(in: 0 ... 5)) { randomAlphanumericString() }, - secrecy: secrecy - ) - - #expect(intVariable.isSecret == (secrecy == .secret)) - #expect(stringVariable.isSecret == [.secret, .auto].contains(secrecy)) - #expect(stringArrayVariable.isSecret == [.secret, .auto].contains(secrecy)) - } -} diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReader+FunctionsTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReader+FunctionsTests.swift new file mode 100644 index 0000000..a403d16 --- /dev/null +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReader+FunctionsTests.swift @@ -0,0 +1,2524 @@ +// +// ConfigVariableReader+FunctionsTests.swift +// DevConfiguration +// +// Generated using gyb. +// + +// swift-format-ignore-file +// +// ############################################################################# +// # # +// # DO NOT EDIT THIS FILE; IT IS AUTOGENERATED. # +// # # +// ############################################################################# + +import Configuration +import DevFoundation +import DevTesting +import Testing + +@testable import DevConfiguration + +struct ConfigVariableReader_FunctionsTests: RandomValueGenerating { + var randomNumberGenerator = makeRandomNumberGenerator() + + /// A mutable provider for testing. + let provider = MutableInMemoryProvider(initialValues: [:]) + + /// The event bus for testing event posting. + let eventBus = EventBus() + + /// The reader under test. + lazy var reader: ConfigVariableReader = { + ConfigVariableReader(providers: [provider], eventBus: eventBus) + }() + + /// Sets a value in the provider for the given key with a random `isSecret` flag. + private mutating func setProviderValue(_ content: ConfigContent, forKey key: ConfigKey) { + provider.setValue( + .init(content, isSecret: randomBool()), + forKey: .init(key) + ) + } + + + // MARK: - Bool tests + + @Test + mutating func valueForBoolReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = BoolTestValues.random(using: &randomNumberGenerator) + let defaultValue = BoolTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(BoolTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForBoolReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = BoolTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForBoolReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = BoolTestValues.random(using: &randomNumberGenerator) + let defaultValue = BoolTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(BoolTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForBoolReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = BoolTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForBoolReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = BoolTestValues.random(using: &randomNumberGenerator) + let updatedValue = BoolTestValues.different(from: initialValue, using: &randomNumberGenerator) + let defaultValue = BoolTestValues.different(from: initialValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(BoolTestValues.configContent(for: initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = try await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init( + BoolTestValues.configContent(for: updatedValue), + isSecret: randomBool() + ), + forKey: .init(key) + ) + + let value2 = try await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptBoolReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = BoolTestValues.random(using: &randomNumberGenerator) + let defaultValue = BoolTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(BoolTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - [Bool] tests + + @Test + mutating func valueForBoolArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = BoolArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = BoolArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[Bool]>(key: key, defaultValue: defaultValue) + setProviderValue(BoolArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForBoolArrayReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = BoolArrayTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[Bool]>(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForBoolArrayReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = BoolArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = BoolArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[Bool]>(key: key, defaultValue: defaultValue) + setProviderValue(BoolArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForBoolArrayReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = BoolArrayTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[Bool]>(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForBoolArrayReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = BoolArrayTestValues.random(using: &randomNumberGenerator) + let updatedValue = BoolArrayTestValues.different(from: initialValue, using: &randomNumberGenerator) + let defaultValue = BoolArrayTestValues.different(from: initialValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[Bool]>(key: key, defaultValue: defaultValue) + setProviderValue(BoolArrayTestValues.configContent(for: initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = try await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init( + BoolArrayTestValues.configContent(for: updatedValue), + isSecret: randomBool() + ), + forKey: .init(key) + ) + + let value2 = try await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptBoolArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = BoolArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = BoolArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[Bool]>(key: key, defaultValue: defaultValue) + setProviderValue(BoolArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - Float64 tests + + @Test + mutating func valueForFloat64ReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = Float64TestValues.random(using: &randomNumberGenerator) + let defaultValue = Float64TestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(Float64TestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForFloat64ReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = Float64TestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForFloat64ReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = Float64TestValues.random(using: &randomNumberGenerator) + let defaultValue = Float64TestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(Float64TestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForFloat64ReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = Float64TestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForFloat64ReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = Float64TestValues.random(using: &randomNumberGenerator) + let updatedValue = Float64TestValues.different(from: initialValue, using: &randomNumberGenerator) + let defaultValue = Float64TestValues.different(from: initialValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(Float64TestValues.configContent(for: initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = try await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init( + Float64TestValues.configContent(for: updatedValue), + isSecret: randomBool() + ), + forKey: .init(key) + ) + + let value2 = try await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptFloat64ReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = Float64TestValues.random(using: &randomNumberGenerator) + let defaultValue = Float64TestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(Float64TestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - [Float64] tests + + @Test + mutating func valueForFloat64ArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = Float64ArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = Float64ArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[Float64]>(key: key, defaultValue: defaultValue) + setProviderValue(Float64ArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForFloat64ArrayReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = Float64ArrayTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[Float64]>(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForFloat64ArrayReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = Float64ArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = Float64ArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[Float64]>(key: key, defaultValue: defaultValue) + setProviderValue(Float64ArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForFloat64ArrayReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = Float64ArrayTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[Float64]>(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForFloat64ArrayReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = Float64ArrayTestValues.random(using: &randomNumberGenerator) + let updatedValue = Float64ArrayTestValues.different(from: initialValue, using: &randomNumberGenerator) + let defaultValue = Float64ArrayTestValues.different(from: initialValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[Float64]>(key: key, defaultValue: defaultValue) + setProviderValue(Float64ArrayTestValues.configContent(for: initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = try await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init( + Float64ArrayTestValues.configContent(for: updatedValue), + isSecret: randomBool() + ), + forKey: .init(key) + ) + + let value2 = try await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptFloat64ArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = Float64ArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = Float64ArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[Float64]>(key: key, defaultValue: defaultValue) + setProviderValue(Float64ArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - Int tests + + @Test + mutating func valueForIntReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = IntTestValues.random(using: &randomNumberGenerator) + let defaultValue = IntTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(IntTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForIntReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = IntTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForIntReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = IntTestValues.random(using: &randomNumberGenerator) + let defaultValue = IntTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(IntTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForIntReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = IntTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForIntReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = IntTestValues.random(using: &randomNumberGenerator) + let updatedValue = IntTestValues.different(from: initialValue, using: &randomNumberGenerator) + let defaultValue = IntTestValues.different(from: initialValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(IntTestValues.configContent(for: initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = try await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init( + IntTestValues.configContent(for: updatedValue), + isSecret: randomBool() + ), + forKey: .init(key) + ) + + let value2 = try await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptIntReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = IntTestValues.random(using: &randomNumberGenerator) + let defaultValue = IntTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(IntTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - [Int] tests + + @Test + mutating func valueForIntArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = IntArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = IntArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[Int]>(key: key, defaultValue: defaultValue) + setProviderValue(IntArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForIntArrayReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = IntArrayTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[Int]>(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForIntArrayReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = IntArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = IntArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[Int]>(key: key, defaultValue: defaultValue) + setProviderValue(IntArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForIntArrayReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = IntArrayTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[Int]>(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForIntArrayReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = IntArrayTestValues.random(using: &randomNumberGenerator) + let updatedValue = IntArrayTestValues.different(from: initialValue, using: &randomNumberGenerator) + let defaultValue = IntArrayTestValues.different(from: initialValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[Int]>(key: key, defaultValue: defaultValue) + setProviderValue(IntArrayTestValues.configContent(for: initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = try await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init( + IntArrayTestValues.configContent(for: updatedValue), + isSecret: randomBool() + ), + forKey: .init(key) + ) + + let value2 = try await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptIntArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = IntArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = IntArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[Int]>(key: key, defaultValue: defaultValue) + setProviderValue(IntArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - String tests + + @Test + mutating func valueForStringReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = StringTestValues.random(using: &randomNumberGenerator) + let defaultValue = StringTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(StringTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForStringReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = StringTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForStringReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = StringTestValues.random(using: &randomNumberGenerator) + let defaultValue = StringTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(StringTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForStringReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = StringTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForStringReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = StringTestValues.random(using: &randomNumberGenerator) + let updatedValue = StringTestValues.different(from: initialValue, using: &randomNumberGenerator) + let defaultValue = StringTestValues.different(from: initialValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(StringTestValues.configContent(for: initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = try await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init( + StringTestValues.configContent(for: updatedValue), + isSecret: randomBool() + ), + forKey: .init(key) + ) + + let value2 = try await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptStringReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = StringTestValues.random(using: &randomNumberGenerator) + let defaultValue = StringTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(StringTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - [String] tests + + @Test + mutating func valueForStringArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = StringArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = StringArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[String]>(key: key, defaultValue: defaultValue) + setProviderValue(StringArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForStringArrayReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = StringArrayTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[String]>(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForStringArrayReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = StringArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = StringArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[String]>(key: key, defaultValue: defaultValue) + setProviderValue(StringArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForStringArrayReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = StringArrayTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[String]>(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForStringArrayReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = StringArrayTestValues.random(using: &randomNumberGenerator) + let updatedValue = StringArrayTestValues.different(from: initialValue, using: &randomNumberGenerator) + let defaultValue = StringArrayTestValues.different(from: initialValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[String]>(key: key, defaultValue: defaultValue) + setProviderValue(StringArrayTestValues.configContent(for: initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = try await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init( + StringArrayTestValues.configContent(for: updatedValue), + isSecret: randomBool() + ), + forKey: .init(key) + ) + + let value2 = try await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptStringArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = StringArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = StringArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[String]>(key: key, defaultValue: defaultValue) + setProviderValue(StringArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - [UInt8] tests + + @Test + mutating func valueForBytesReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = BytesTestValues.random(using: &randomNumberGenerator) + let defaultValue = BytesTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[UInt8]>(key: key, defaultValue: defaultValue) + setProviderValue(BytesTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForBytesReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = BytesTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[UInt8]>(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForBytesReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = BytesTestValues.random(using: &randomNumberGenerator) + let defaultValue = BytesTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[UInt8]>(key: key, defaultValue: defaultValue) + setProviderValue(BytesTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForBytesReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = BytesTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[UInt8]>(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForBytesReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = BytesTestValues.random(using: &randomNumberGenerator) + let updatedValue = BytesTestValues.different(from: initialValue, using: &randomNumberGenerator) + let defaultValue = BytesTestValues.different(from: initialValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[UInt8]>(key: key, defaultValue: defaultValue) + setProviderValue(BytesTestValues.configContent(for: initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = try await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init( + BytesTestValues.configContent(for: updatedValue), + isSecret: randomBool() + ), + forKey: .init(key) + ) + + let value2 = try await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptBytesReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = BytesTestValues.random(using: &randomNumberGenerator) + let defaultValue = BytesTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[UInt8]>(key: key, defaultValue: defaultValue) + setProviderValue(BytesTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - [[UInt8]] tests + + @Test + mutating func valueForByteChunkArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = ByteChunkArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = ByteChunkArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[[UInt8]]>(key: key, defaultValue: defaultValue) + setProviderValue(ByteChunkArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForByteChunkArrayReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = ByteChunkArrayTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[[UInt8]]>(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForByteChunkArrayReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = ByteChunkArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = ByteChunkArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[[UInt8]]>(key: key, defaultValue: defaultValue) + setProviderValue(ByteChunkArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForByteChunkArrayReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = ByteChunkArrayTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[[UInt8]]>(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForByteChunkArrayReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = ByteChunkArrayTestValues.random(using: &randomNumberGenerator) + let updatedValue = ByteChunkArrayTestValues.different(from: initialValue, using: &randomNumberGenerator) + let defaultValue = ByteChunkArrayTestValues.different(from: initialValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[[UInt8]]>(key: key, defaultValue: defaultValue) + setProviderValue(ByteChunkArrayTestValues.configContent(for: initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = try await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init( + ByteChunkArrayTestValues.configContent(for: updatedValue), + isSecret: randomBool() + ), + forKey: .init(key) + ) + + let value2 = try await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptByteChunkArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = ByteChunkArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = ByteChunkArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[[UInt8]]>(key: key, defaultValue: defaultValue) + setProviderValue(ByteChunkArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - RawRepresentable tests + + @Test + mutating func valueForRawRepresentableStringReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = RawRepresentableStringTestValues.random(using: &randomNumberGenerator) + let defaultValue = RawRepresentableStringTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(RawRepresentableStringTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForRawRepresentableStringReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = RawRepresentableStringTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForRawRepresentableStringReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = RawRepresentableStringTestValues.random(using: &randomNumberGenerator) + let defaultValue = RawRepresentableStringTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(RawRepresentableStringTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForRawRepresentableStringReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = RawRepresentableStringTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForRawRepresentableStringReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = RawRepresentableStringTestValues.random(using: &randomNumberGenerator) + let updatedValue = RawRepresentableStringTestValues.different(from: initialValue, using: &randomNumberGenerator) + let defaultValue = RawRepresentableStringTestValues.different(from: initialValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(RawRepresentableStringTestValues.configContent(for: initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = try await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init( + RawRepresentableStringTestValues.configContent(for: updatedValue), + isSecret: randomBool() + ), + forKey: .init(key) + ) + + let value2 = try await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptRawRepresentableStringReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = RawRepresentableStringTestValues.random(using: &randomNumberGenerator) + let defaultValue = RawRepresentableStringTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(RawRepresentableStringTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - [RawRepresentable] tests + + @Test + mutating func valueForRawRepresentableStringArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = RawRepresentableStringArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = RawRepresentableStringArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[MockStringEnum]>(key: key, defaultValue: defaultValue) + setProviderValue(RawRepresentableStringArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForRawRepresentableStringArrayReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = RawRepresentableStringArrayTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[MockStringEnum]>(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForRawRepresentableStringArrayReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = RawRepresentableStringArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = RawRepresentableStringArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[MockStringEnum]>(key: key, defaultValue: defaultValue) + setProviderValue(RawRepresentableStringArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForRawRepresentableStringArrayReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = RawRepresentableStringArrayTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[MockStringEnum]>(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForRawRepresentableStringArrayReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = RawRepresentableStringArrayTestValues.random(using: &randomNumberGenerator) + let updatedValue = RawRepresentableStringArrayTestValues.different(from: initialValue, using: &randomNumberGenerator) + let defaultValue = RawRepresentableStringArrayTestValues.different(from: initialValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[MockStringEnum]>(key: key, defaultValue: defaultValue) + setProviderValue(RawRepresentableStringArrayTestValues.configContent(for: initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = try await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init( + RawRepresentableStringArrayTestValues.configContent(for: updatedValue), + isSecret: randomBool() + ), + forKey: .init(key) + ) + + let value2 = try await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptRawRepresentableStringArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = RawRepresentableStringArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = RawRepresentableStringArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[MockStringEnum]>(key: key, defaultValue: defaultValue) + setProviderValue(RawRepresentableStringArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - ExpressibleByConfigString tests + + @Test + mutating func valueForExpressibleByConfigStringReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = ExpressibleByConfigStringTestValues.random(using: &randomNumberGenerator) + let defaultValue = ExpressibleByConfigStringTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(ExpressibleByConfigStringTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForExpressibleByConfigStringReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = ExpressibleByConfigStringTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForExpressibleByConfigStringReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = ExpressibleByConfigStringTestValues.random(using: &randomNumberGenerator) + let defaultValue = ExpressibleByConfigStringTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(ExpressibleByConfigStringTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForExpressibleByConfigStringReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = ExpressibleByConfigStringTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForExpressibleByConfigStringReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = ExpressibleByConfigStringTestValues.random(using: &randomNumberGenerator) + let updatedValue = ExpressibleByConfigStringTestValues.different(from: initialValue, using: &randomNumberGenerator) + let defaultValue = ExpressibleByConfigStringTestValues.different(from: initialValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(ExpressibleByConfigStringTestValues.configContent(for: initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = try await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init( + ExpressibleByConfigStringTestValues.configContent(for: updatedValue), + isSecret: randomBool() + ), + forKey: .init(key) + ) + + let value2 = try await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptExpressibleByConfigStringReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = ExpressibleByConfigStringTestValues.random(using: &randomNumberGenerator) + let defaultValue = ExpressibleByConfigStringTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(ExpressibleByConfigStringTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - [ExpressibleByConfigString] tests + + @Test + mutating func valueForExpressibleByConfigStringArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = ExpressibleByConfigStringArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = ExpressibleByConfigStringArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[MockConfigStringValue]>(key: key, defaultValue: defaultValue) + setProviderValue(ExpressibleByConfigStringArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForExpressibleByConfigStringArrayReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = ExpressibleByConfigStringArrayTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[MockConfigStringValue]>(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForExpressibleByConfigStringArrayReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = ExpressibleByConfigStringArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = ExpressibleByConfigStringArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[MockConfigStringValue]>(key: key, defaultValue: defaultValue) + setProviderValue(ExpressibleByConfigStringArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForExpressibleByConfigStringArrayReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = ExpressibleByConfigStringArrayTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[MockConfigStringValue]>(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForExpressibleByConfigStringArrayReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = ExpressibleByConfigStringArrayTestValues.random(using: &randomNumberGenerator) + let updatedValue = ExpressibleByConfigStringArrayTestValues.different(from: initialValue, using: &randomNumberGenerator) + let defaultValue = ExpressibleByConfigStringArrayTestValues.different(from: initialValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[MockConfigStringValue]>(key: key, defaultValue: defaultValue) + setProviderValue(ExpressibleByConfigStringArrayTestValues.configContent(for: initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = try await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init( + ExpressibleByConfigStringArrayTestValues.configContent(for: updatedValue), + isSecret: randomBool() + ), + forKey: .init(key) + ) + + let value2 = try await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptExpressibleByConfigStringArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = ExpressibleByConfigStringArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = ExpressibleByConfigStringArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[MockConfigStringValue]>(key: key, defaultValue: defaultValue) + setProviderValue(ExpressibleByConfigStringArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - RawRepresentable tests + + @Test + mutating func valueForRawRepresentableIntReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = RawRepresentableIntTestValues.random(using: &randomNumberGenerator) + let defaultValue = RawRepresentableIntTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(RawRepresentableIntTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForRawRepresentableIntReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = RawRepresentableIntTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForRawRepresentableIntReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = RawRepresentableIntTestValues.random(using: &randomNumberGenerator) + let defaultValue = RawRepresentableIntTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(RawRepresentableIntTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForRawRepresentableIntReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = RawRepresentableIntTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForRawRepresentableIntReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = RawRepresentableIntTestValues.random(using: &randomNumberGenerator) + let updatedValue = RawRepresentableIntTestValues.different(from: initialValue, using: &randomNumberGenerator) + let defaultValue = RawRepresentableIntTestValues.different(from: initialValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(RawRepresentableIntTestValues.configContent(for: initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = try await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init( + RawRepresentableIntTestValues.configContent(for: updatedValue), + isSecret: randomBool() + ), + forKey: .init(key) + ) + + let value2 = try await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptRawRepresentableIntReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = RawRepresentableIntTestValues.random(using: &randomNumberGenerator) + let defaultValue = RawRepresentableIntTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(RawRepresentableIntTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - [RawRepresentable] tests + + @Test + mutating func valueForRawRepresentableIntArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = RawRepresentableIntArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = RawRepresentableIntArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[MockIntEnum]>(key: key, defaultValue: defaultValue) + setProviderValue(RawRepresentableIntArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForRawRepresentableIntArrayReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = RawRepresentableIntArrayTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[MockIntEnum]>(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForRawRepresentableIntArrayReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = RawRepresentableIntArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = RawRepresentableIntArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[MockIntEnum]>(key: key, defaultValue: defaultValue) + setProviderValue(RawRepresentableIntArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForRawRepresentableIntArrayReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = RawRepresentableIntArrayTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[MockIntEnum]>(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForRawRepresentableIntArrayReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = RawRepresentableIntArrayTestValues.random(using: &randomNumberGenerator) + let updatedValue = RawRepresentableIntArrayTestValues.different(from: initialValue, using: &randomNumberGenerator) + let defaultValue = RawRepresentableIntArrayTestValues.different(from: initialValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[MockIntEnum]>(key: key, defaultValue: defaultValue) + setProviderValue(RawRepresentableIntArrayTestValues.configContent(for: initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = try await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init( + RawRepresentableIntArrayTestValues.configContent(for: updatedValue), + isSecret: randomBool() + ), + forKey: .init(key) + ) + + let value2 = try await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptRawRepresentableIntArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = RawRepresentableIntArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = RawRepresentableIntArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[MockIntEnum]>(key: key, defaultValue: defaultValue) + setProviderValue(RawRepresentableIntArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - ExpressibleByConfigInt tests + + @Test + mutating func valueForExpressibleByConfigIntReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = ExpressibleByConfigIntTestValues.random(using: &randomNumberGenerator) + let defaultValue = ExpressibleByConfigIntTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(ExpressibleByConfigIntTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForExpressibleByConfigIntReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = ExpressibleByConfigIntTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForExpressibleByConfigIntReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = ExpressibleByConfigIntTestValues.random(using: &randomNumberGenerator) + let defaultValue = ExpressibleByConfigIntTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(ExpressibleByConfigIntTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForExpressibleByConfigIntReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = ExpressibleByConfigIntTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForExpressibleByConfigIntReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = ExpressibleByConfigIntTestValues.random(using: &randomNumberGenerator) + let updatedValue = ExpressibleByConfigIntTestValues.different(from: initialValue, using: &randomNumberGenerator) + let defaultValue = ExpressibleByConfigIntTestValues.different(from: initialValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(ExpressibleByConfigIntTestValues.configContent(for: initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = try await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init( + ExpressibleByConfigIntTestValues.configContent(for: updatedValue), + isSecret: randomBool() + ), + forKey: .init(key) + ) + + let value2 = try await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptExpressibleByConfigIntReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = ExpressibleByConfigIntTestValues.random(using: &randomNumberGenerator) + let defaultValue = ExpressibleByConfigIntTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable(key: key, defaultValue: defaultValue) + setProviderValue(ExpressibleByConfigIntTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - [ExpressibleByConfigInt] tests + + @Test + mutating func valueForExpressibleByConfigIntArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = ExpressibleByConfigIntArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = ExpressibleByConfigIntArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[MockConfigIntValue]>(key: key, defaultValue: defaultValue) + setProviderValue(ExpressibleByConfigIntArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func valueForExpressibleByConfigIntArrayReturnsDefaultWhenKeyNotFound() { + // set up + let key = randomConfigKey() + let defaultValue = ExpressibleByConfigIntArrayTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[MockConfigIntValue]>(key: key, defaultValue: defaultValue) + + // exercise + let result = reader.value(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func fetchValueForExpressibleByConfigIntArrayReturnsProviderValue() async throws { + // set up + let key = randomConfigKey() + let expectedValue = ExpressibleByConfigIntArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = ExpressibleByConfigIntArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[MockConfigIntValue]>(key: key, defaultValue: defaultValue) + setProviderValue(ExpressibleByConfigIntArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == expectedValue) + } + + + @Test + mutating func fetchValueForExpressibleByConfigIntArrayReturnsDefaultWhenKeyNotFound() async throws { + // set up + let key = randomConfigKey() + let defaultValue = ExpressibleByConfigIntArrayTestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<[MockConfigIntValue]>(key: key, defaultValue: defaultValue) + + // exercise + let result = try await reader.fetchValue(for: variable) + + // expect + #expect(result == defaultValue) + } + + + @Test + mutating func watchValueForExpressibleByConfigIntArrayReceivesUpdates() async throws { + // set up + let key = randomConfigKey() + let initialValue = ExpressibleByConfigIntArrayTestValues.random(using: &randomNumberGenerator) + let updatedValue = ExpressibleByConfigIntArrayTestValues.different(from: initialValue, using: &randomNumberGenerator) + let defaultValue = ExpressibleByConfigIntArrayTestValues.different(from: initialValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[MockConfigIntValue]>(key: key, defaultValue: defaultValue) + setProviderValue(ExpressibleByConfigIntArrayTestValues.configContent(for: initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = try await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init( + ExpressibleByConfigIntArrayTestValues.configContent(for: updatedValue), + isSecret: randomBool() + ), + forKey: .init(key) + ) + + let value2 = try await iterator.next() + #expect(value2 == updatedValue) + } + } + + + @Test + mutating func subscriptExpressibleByConfigIntArrayReturnsProviderValue() { + // set up + let key = randomConfigKey() + let expectedValue = ExpressibleByConfigIntArrayTestValues.random(using: &randomNumberGenerator) + let defaultValue = ExpressibleByConfigIntArrayTestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<[MockConfigIntValue]>(key: key, defaultValue: defaultValue) + setProviderValue(ExpressibleByConfigIntArrayTestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = reader[variable] + + // expect + #expect(result == expectedValue) + } + + + // MARK: - Event Bus Integration + + @Test + mutating func valuePostsAccessSucceededEventWhenFound() async throws { + // set up + let observer = ContextualBusEventObserver(context: ()) + eventBus.addObserver(observer) + + let key = randomConfigKey() + let expectedValue = randomBool() + let variable = ConfigVariable(key: key, defaultValue: !expectedValue) + provider.setValue( + .init( + .bool(expectedValue), + isSecret: randomBool() + ), + forKey: .init(variable.key) + ) + + let (eventStream, continuation) = AsyncStream.makeStream() + observer.addHandler(for: ConfigVariableAccessSucceededEvent.self) { (event, _) in + continuation.yield(event) + } + + // exercise + _ = reader.value(for: variable) + + // expect + let postedEvent = try #require(await eventStream.first { _ in true }) + #expect(postedEvent.key == AbsoluteConfigKey(variable.key)) + #expect(postedEvent.value.content == .bool(expectedValue)) + } +} + + +// MARK: - Primitive TestValues + +private enum BoolTestValues { + static func random(using generator: inout some RandomNumberGenerator) -> Bool { + Bool.random(using: &generator) + } + + + static func different(from value: Bool, using generator: inout some RandomNumberGenerator) -> Bool { + !value + } + + + static func configContent(for value: Bool) -> ConfigContent { + .bool(value) + } +} + + +private enum BoolArrayTestValues { + static func random(using generator: inout some RandomNumberGenerator) -> [Bool] { + let count = Int.random(in: 1 ... 5, using: &generator) + return Array(count: count) { Bool.random(using: &generator) } + } + + + static func different(from value: [Bool], using generator: inout some RandomNumberGenerator) -> [Bool] { + value + random(using: &generator) + } + + + static func configContent(for value: [Bool]) -> ConfigContent { + .boolArray(value) + } +} + + +private enum Float64TestValues { + static func random(using generator: inout some RandomNumberGenerator) -> Float64 { + Float64.random(in: 1 ... 100_000, using: &generator) + } + + + static func different(from value: Float64, using generator: inout some RandomNumberGenerator) -> Float64 { + value + random(using: &generator) + } + + + static func configContent(for value: Float64) -> ConfigContent { + .double(value) + } +} + + +private enum Float64ArrayTestValues { + static func random(using generator: inout some RandomNumberGenerator) -> [Float64] { + let count = Int.random(in: 1 ... 5, using: &generator) + return Array(count: count) { Float64.random(in: 1 ... 100_000, using: &generator) } + } + + + static func different(from value: [Float64], using generator: inout some RandomNumberGenerator) -> [Float64] { + value + random(using: &generator) + } + + + static func configContent(for value: [Float64]) -> ConfigContent { + .doubleArray(value) + } +} + + +private enum IntTestValues { + static func random(using generator: inout some RandomNumberGenerator) -> Int { + Int.random(in: 1 ... 100_000, using: &generator) + } + + + static func different(from value: Int, using generator: inout some RandomNumberGenerator) -> Int { + value + random(using: &generator) + } + + + static func configContent(for value: Int) -> ConfigContent { + .int(value) + } +} + + +private enum IntArrayTestValues { + static func random(using generator: inout some RandomNumberGenerator) -> [Int] { + let count = Int.random(in: 1 ... 5, using: &generator) + return Array(count: count) { Int.random(in: 1 ... 100_000, using: &generator) } + } + + + static func different(from value: [Int], using generator: inout some RandomNumberGenerator) -> [Int] { + value + random(using: &generator) + } + + + static func configContent(for value: [Int]) -> ConfigContent { + .intArray(value) + } +} + + +private enum StringTestValues { + static func random(using generator: inout some RandomNumberGenerator) -> String { + let count = Int.random(in: 5 ... 20, using: &generator) + return String.randomAlphanumeric(count: count, using: &generator) + } + + + static func different(from value: String, using generator: inout some RandomNumberGenerator) -> String { + value + random(using: &generator) + } + + + static func configContent(for value: String) -> ConfigContent { + .string(value) + } +} + + +private enum StringArrayTestValues { + static func random(using generator: inout some RandomNumberGenerator) -> [String] { + let count = Int.random(in: 1 ... 5, using: &generator) + return Array(count: count) { String.randomAlphanumeric(count: count * 3, using: &generator) } + } + + + static func different(from value: [String], using generator: inout some RandomNumberGenerator) -> [String] { + value + random(using: &generator) + } + + + static func configContent(for value: [String]) -> ConfigContent { + .stringArray(value) + } +} + + +private enum BytesTestValues { + static func random(using generator: inout some RandomNumberGenerator) -> [UInt8] { + let count = Int.random(in: 1 ... 32, using: &generator) + return Array(count: count) { UInt8.random(in: .min ... .max, using: &generator) } + } + + + static func different(from value: [UInt8], using generator: inout some RandomNumberGenerator) -> [UInt8] { + value + random(using: &generator) + } + + + static func configContent(for value: [UInt8]) -> ConfigContent { + .bytes(value) + } +} + + +private enum ByteChunkArrayTestValues { + static func random(using generator: inout some RandomNumberGenerator) -> [[UInt8]] { + let count = Int.random(in: 1 ... 5, using: &generator) + return Array(count: count) { + let byteCount = Int.random(in: 1 ... 32, using: &generator) + return Array(count: byteCount) { UInt8.random(in: .min ... .max, using: &generator) } + } + } + + + static func different(from value: [[UInt8]], using generator: inout some RandomNumberGenerator) -> [[UInt8]] { + value + random(using: &generator) + } + + + static func configContent(for value: [[UInt8]]) -> ConfigContent { + .byteChunkArray(value) + } +} + + + +// MARK: - MockStringEnum + +private enum MockStringEnum: String, CaseIterable, Sendable { + case alpha + case bravo + case charlie + case delta +} + + +// MARK: - MockConfigStringValue + +private struct MockConfigStringValue: ExpressibleByConfigString, Hashable, Sendable { + let stringValue: String + + var description: String { stringValue } + + init?(configString: String) { + self.stringValue = configString + } +} + + +// MARK: - String-Convertible TestValues + +private enum RawRepresentableStringTestValues { + static func random(using generator: inout some RandomNumberGenerator) -> MockStringEnum { + MockStringEnum.allCases.randomElement(using: &generator)! + } + + + static func different(from value: MockStringEnum, using generator: inout some RandomNumberGenerator) -> MockStringEnum { + var result: MockStringEnum + repeat { + result = random(using: &generator) + } while result == value + return result + } + + + static func configContent(for value: MockStringEnum) -> ConfigContent { + .string(value.rawValue) + } +} + + +private enum RawRepresentableStringArrayTestValues { + static func random(using generator: inout some RandomNumberGenerator) -> [MockStringEnum] { + let count = Int.random(in: 1 ... 5, using: &generator) + return Array(count: count) { MockStringEnum.allCases.randomElement(using: &generator)! } + } + + + static func different(from value: [MockStringEnum], using generator: inout some RandomNumberGenerator) -> [MockStringEnum] { + value + random(using: &generator) + } + + + static func configContent(for value: [MockStringEnum]) -> ConfigContent { + .stringArray(value.map(\.rawValue)) + } +} + + +private enum ExpressibleByConfigStringTestValues { + static func random(using generator: inout some RandomNumberGenerator) -> MockConfigStringValue { + let count = Int.random(in: 5 ... 20, using: &generator) + let string = String.randomAlphanumeric(count: count, using: &generator) + return MockConfigStringValue(configString: string)! + } + + + static func different(from value: MockConfigStringValue, using generator: inout some RandomNumberGenerator) -> MockConfigStringValue { + let suffix = random(using: &generator) + return MockConfigStringValue(configString: value.stringValue + suffix.stringValue)! + } + + + static func configContent(for value: MockConfigStringValue) -> ConfigContent { + .string(value.description) + } +} + + +private enum ExpressibleByConfigStringArrayTestValues { + static func random(using generator: inout some RandomNumberGenerator) -> [MockConfigStringValue] { + let count = Int.random(in: 1 ... 5, using: &generator) + return Array(count: count) { + let length = Int.random(in: 5 ... 20, using: &generator) + return MockConfigStringValue(configString: String.randomAlphanumeric(count: length, using: &generator))! + } + } + + + static func different(from value: [MockConfigStringValue], using generator: inout some RandomNumberGenerator) -> [MockConfigStringValue] { + value + random(using: &generator) + } + + + static func configContent(for value: [MockConfigStringValue]) -> ConfigContent { + .stringArray(value.map(\.description)) + } +} + + + +// MARK: - MockIntEnum + +private enum MockIntEnum: Int, CaseIterable, Sendable { + case one = 1 + case two = 2 + case three = 3 + case four = 4 +} + + +// MARK: - MockConfigIntValue + +private struct MockConfigIntValue: ExpressibleByConfigInt, Hashable, Sendable { + let intValue: Int + + var configInt: Int { intValue } + + var description: String { "\(intValue)" } + + init?(configInt: Int) { + self.intValue = configInt + } +} + + +// MARK: - Int-Convertible TestValues + +private enum RawRepresentableIntTestValues { + static func random(using generator: inout some RandomNumberGenerator) -> MockIntEnum { + MockIntEnum.allCases.randomElement(using: &generator)! + } + + + static func different(from value: MockIntEnum, using generator: inout some RandomNumberGenerator) -> MockIntEnum { + var result: MockIntEnum + repeat { + result = random(using: &generator) + } while result == value + return result + } + + + static func configContent(for value: MockIntEnum) -> ConfigContent { + .int(value.rawValue) + } +} + + +private enum RawRepresentableIntArrayTestValues { + static func random(using generator: inout some RandomNumberGenerator) -> [MockIntEnum] { + let count = Int.random(in: 1 ... 5, using: &generator) + return Array(count: count) { MockIntEnum.allCases.randomElement(using: &generator)! } + } + + + static func different(from value: [MockIntEnum], using generator: inout some RandomNumberGenerator) -> [MockIntEnum] { + value + random(using: &generator) + } + + + static func configContent(for value: [MockIntEnum]) -> ConfigContent { + .intArray(value.map(\.rawValue)) + } +} + + +private enum ExpressibleByConfigIntTestValues { + static func random(using generator: inout some RandomNumberGenerator) -> MockConfigIntValue { + MockConfigIntValue(configInt: Int.random(in: 1 ... 100_000, using: &generator))! + } + + + static func different(from value: MockConfigIntValue, using generator: inout some RandomNumberGenerator) -> MockConfigIntValue { + MockConfigIntValue(configInt: value.intValue + random(using: &generator).intValue)! + } + + + static func configContent(for value: MockConfigIntValue) -> ConfigContent { + .int(value.configInt) + } +} + + +private enum ExpressibleByConfigIntArrayTestValues { + static func random(using generator: inout some RandomNumberGenerator) -> [MockConfigIntValue] { + let count = Int.random(in: 1 ... 5, using: &generator) + return Array(count: count) { + MockConfigIntValue(configInt: Int.random(in: 1 ... 100_000, using: &generator))! + } + } + + + static func different(from value: [MockConfigIntValue], using generator: inout some RandomNumberGenerator) -> [MockConfigIntValue] { + value + random(using: &generator) + } + + + static func configContent(for value: [MockConfigIntValue]) -> ConfigContent { + .intArray(value.map(\.configInt)) + } +} + + diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReader+FunctionsTests.swift.gyb b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReader+FunctionsTests.swift.gyb new file mode 100644 index 0000000..345d1dc --- /dev/null +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReader+FunctionsTests.swift.gyb @@ -0,0 +1,553 @@ +%{ +import sys +import os + +scripts_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', '..', '..', 'Scripts', 'gyb') +sys.path.insert(0, scripts_dir) + +from gyb_utils import autogenerated_warning + + +# Each helper defines the type-specific test value generation for a config value type. +# +# - name: Name component used in test methods and TestValues enum names +# - type: The Swift type +# - mark: The MARK comment label +# - random: Lines of the random value generation body +# - different: Lines of the different value generation body +# - content: Lines of the configContent conversion body + +primitive_helpers = [ + { + "name": "Bool", + "type": "Bool", + "mark": "Bool", + "random": ["Bool.random(using: &generator)"], + "different": ["!value"], + "content": [".bool(value)"], + }, + { + "name": "BoolArray", + "type": "[Bool]", + "mark": "[Bool]", + "random": [ + "let count = Int.random(in: 1 ... 5, using: &generator)", + "return Array(count: count) { Bool.random(using: &generator) }", + ], + "different": ["value + random(using: &generator)"], + "content": [".boolArray(value)"], + }, + { + "name": "Float64", + "type": "Float64", + "mark": "Float64", + "random": ["Float64.random(in: 1 ... 100_000, using: &generator)"], + "different": ["value + random(using: &generator)"], + "content": [".double(value)"], + }, + { + "name": "Float64Array", + "type": "[Float64]", + "mark": "[Float64]", + "random": [ + "let count = Int.random(in: 1 ... 5, using: &generator)", + "return Array(count: count) { Float64.random(in: 1 ... 100_000, using: &generator) }", + ], + "different": ["value + random(using: &generator)"], + "content": [".doubleArray(value)"], + }, + { + "name": "Int", + "type": "Int", + "mark": "Int", + "random": ["Int.random(in: 1 ... 100_000, using: &generator)"], + "different": ["value + random(using: &generator)"], + "content": [".int(value)"], + }, + { + "name": "IntArray", + "type": "[Int]", + "mark": "[Int]", + "random": [ + "let count = Int.random(in: 1 ... 5, using: &generator)", + "return Array(count: count) { Int.random(in: 1 ... 100_000, using: &generator) }", + ], + "different": ["value + random(using: &generator)"], + "content": [".intArray(value)"], + }, + { + "name": "String", + "type": "String", + "mark": "String", + "random": [ + "let count = Int.random(in: 5 ... 20, using: &generator)", + "return String.randomAlphanumeric(count: count, using: &generator)", + ], + "different": ["value + random(using: &generator)"], + "content": [".string(value)"], + }, + { + "name": "StringArray", + "type": "[String]", + "mark": "[String]", + "random": [ + "let count = Int.random(in: 1 ... 5, using: &generator)", + "return Array(count: count) { String.randomAlphanumeric(count: count * 3, using: &generator) }", + ], + "different": ["value + random(using: &generator)"], + "content": [".stringArray(value)"], + }, + { + "name": "Bytes", + "type": "[UInt8]", + "mark": "[UInt8]", + "random": [ + "let count = Int.random(in: 1 ... 32, using: &generator)", + "return Array(count: count) { UInt8.random(in: .min ... .max, using: &generator) }", + ], + "different": ["value + random(using: &generator)"], + "content": [".bytes(value)"], + }, + { + "name": "ByteChunkArray", + "type": "[[UInt8]]", + "mark": "[[UInt8]]", + "random": [ + "let count = Int.random(in: 1 ... 5, using: &generator)", + "return Array(count: count) {", + " let byteCount = Int.random(in: 1 ... 32, using: &generator)", + " return Array(count: byteCount) { UInt8.random(in: .min ... .max, using: &generator) }", + "}", + ], + "different": ["value + random(using: &generator)"], + "content": [".byteChunkArray(value)"], + }, +] + + +string_convertible_helpers = [ + { + "name": "RawRepresentableString", + "type": "MockStringEnum", + "mark": "RawRepresentable", + "random": ["MockStringEnum.allCases.randomElement(using: &generator)!"], + "different": [ + "var result: MockStringEnum", + "repeat {", + " result = random(using: &generator)", + "} while result == value", + "return result", + ], + "content": [".string(value.rawValue)"], + }, + { + "name": "RawRepresentableStringArray", + "type": "[MockStringEnum]", + "mark": "[RawRepresentable]", + "random": [ + "let count = Int.random(in: 1 ... 5, using: &generator)", + "return Array(count: count) { MockStringEnum.allCases.randomElement(using: &generator)! }", + ], + "different": ["value + random(using: &generator)"], + "content": [".stringArray(value.map(\\.rawValue))"], + }, + { + "name": "ExpressibleByConfigString", + "type": "MockConfigStringValue", + "mark": "ExpressibleByConfigString", + "random": [ + "let count = Int.random(in: 5 ... 20, using: &generator)", + "let string = String.randomAlphanumeric(count: count, using: &generator)", + "return MockConfigStringValue(configString: string)!", + ], + "different": [ + "let suffix = random(using: &generator)", + "return MockConfigStringValue(configString: value.stringValue + suffix.stringValue)!", + ], + "content": [".string(value.description)"], + }, + { + "name": "ExpressibleByConfigStringArray", + "type": "[MockConfigStringValue]", + "mark": "[ExpressibleByConfigString]", + "random": [ + "let count = Int.random(in: 1 ... 5, using: &generator)", + "return Array(count: count) {", + " let length = Int.random(in: 5 ... 20, using: &generator)", + " return MockConfigStringValue(configString: String.randomAlphanumeric(count: length, using: &generator))!", + "}", + ], + "different": ["value + random(using: &generator)"], + "content": [".stringArray(value.map(\\.description))"], + }, +] + + +int_convertible_helpers = [ + { + "name": "RawRepresentableInt", + "type": "MockIntEnum", + "mark": "RawRepresentable", + "random": ["MockIntEnum.allCases.randomElement(using: &generator)!"], + "different": [ + "var result: MockIntEnum", + "repeat {", + " result = random(using: &generator)", + "} while result == value", + "return result", + ], + "content": [".int(value.rawValue)"], + }, + { + "name": "RawRepresentableIntArray", + "type": "[MockIntEnum]", + "mark": "[RawRepresentable]", + "random": [ + "let count = Int.random(in: 1 ... 5, using: &generator)", + "return Array(count: count) { MockIntEnum.allCases.randomElement(using: &generator)! }", + ], + "different": ["value + random(using: &generator)"], + "content": [".intArray(value.map(\\.rawValue))"], + }, + { + "name": "ExpressibleByConfigInt", + "type": "MockConfigIntValue", + "mark": "ExpressibleByConfigInt", + "random": ["MockConfigIntValue(configInt: Int.random(in: 1 ... 100_000, using: &generator))!"], + "different": ["MockConfigIntValue(configInt: value.intValue + random(using: &generator).intValue)!"], + "content": [".int(value.configInt)"], + }, + { + "name": "ExpressibleByConfigIntArray", + "type": "[MockConfigIntValue]", + "mark": "[ExpressibleByConfigInt]", + "random": [ + "let count = Int.random(in: 1 ... 5, using: &generator)", + "return Array(count: count) {", + " MockConfigIntValue(configInt: Int.random(in: 1 ... 100_000, using: &generator))!", + "}", + ], + "different": ["value + random(using: &generator)"], + "content": [".intArray(value.map(\\.configInt))"], + }, +] + + +all_helpers = primitive_helpers + string_convertible_helpers + int_convertible_helpers + + +# Test patterns for cross-product generation. Each entry generates a test method for every value type. +# +# - func_name: Test function name template ({name} replaced with helper name) +# - sig: Function signature suffix +# - call: The expression that reads from the reader (None for watch) +# - pattern: "provider", "default", or "watch" determines the test body +api_tests = [ + { + "func_name": "valueFor{name}ReturnsProviderValue", + "sig": "", + "call": "reader.value(for: variable)", + "pattern": "provider", + }, + { + "func_name": "valueFor{name}ReturnsDefaultWhenKeyNotFound", + "sig": "", + "call": "reader.value(for: variable)", + "pattern": "default", + }, + { + "func_name": "fetchValueFor{name}ReturnsProviderValue", + "sig": " async throws", + "call": "try await reader.fetchValue(for: variable)", + "pattern": "provider", + }, + { + "func_name": "fetchValueFor{name}ReturnsDefaultWhenKeyNotFound", + "sig": " async throws", + "call": "try await reader.fetchValue(for: variable)", + "pattern": "default", + }, + { + "func_name": "watchValueFor{name}ReceivesUpdates", + "sig": " async throws", + "call": None, + "pattern": "watch", + }, + { + "func_name": "subscript{name}ReturnsProviderValue", + "sig": "", + "call": "reader[variable]", + "pattern": "provider", + }, +] +}% +// +// ConfigVariableReader+FunctionsTests.swift +// DevConfiguration +// +// Generated using gyb. +// +${autogenerated_warning()} +import Configuration +import DevFoundation +import DevTesting +import Testing + +@testable import DevConfiguration + +struct ConfigVariableReader_FunctionsTests: RandomValueGenerating { + var randomNumberGenerator = makeRandomNumberGenerator() + + /// A mutable provider for testing. + let provider = MutableInMemoryProvider(initialValues: [:]) + + /// The event bus for testing event posting. + let eventBus = EventBus() + + /// The reader under test. + lazy var reader: ConfigVariableReader = { + ConfigVariableReader(providers: [provider], eventBus: eventBus) + }() + + /// Sets a value in the provider for the given key with a random `isSecret` flag. + private mutating func setProviderValue(_ content: ConfigContent, forKey key: ConfigKey) { + provider.setValue( + .init(content, isSecret: randomBool()), + forKey: .init(key) + ) + } + % for h in all_helpers: + + + // MARK: - ${h["mark"]} tests + + % for i, t in enumerate(api_tests): + % if i > 0: + + + % end + @Test + mutating func ${t["func_name"].format(name=h["name"])}()${t["sig"]} { + % if t["pattern"] == "provider": + // set up + let key = randomConfigKey() + let expectedValue = ${h["name"]}TestValues.random(using: &randomNumberGenerator) + let defaultValue = ${h["name"]}TestValues.different(from: expectedValue, using: &randomNumberGenerator) + let variable = ConfigVariable<${h["type"]}>(key: key, defaultValue: defaultValue) + setProviderValue(${h["name"]}TestValues.configContent(for: expectedValue), forKey: key) + + // exercise + let result = ${t["call"]} + + // expect + #expect(result == expectedValue) + % elif t["pattern"] == "default": + // set up + let key = randomConfigKey() + let defaultValue = ${h["name"]}TestValues.random(using: &randomNumberGenerator) + let variable = ConfigVariable<${h["type"]}>(key: key, defaultValue: defaultValue) + + // exercise + let result = ${t["call"]} + + // expect + #expect(result == defaultValue) + % else: + // set up + let key = randomConfigKey() + let initialValue = ${h["name"]}TestValues.random(using: &randomNumberGenerator) + let updatedValue = ${h["name"]}TestValues.different(from: initialValue, using: &randomNumberGenerator) + let defaultValue = ${h["name"]}TestValues.different(from: initialValue, using: &randomNumberGenerator) + let variable = ConfigVariable<${h["type"]}>(key: key, defaultValue: defaultValue) + setProviderValue(${h["name"]}TestValues.configContent(for: initialValue), forKey: key) + + // exercise and expect + try await reader.watchValue(for: variable) { updates in + var iterator = updates.makeAsyncIterator() + + let value1 = try await iterator.next() + #expect(value1 == initialValue) + + provider.setValue( + .init( + ${h["name"]}TestValues.configContent(for: updatedValue), + isSecret: randomBool() + ), + forKey: .init(key) + ) + + let value2 = try await iterator.next() + #expect(value2 == updatedValue) + } + % end + } + % end + % end + + + // MARK: - Event Bus Integration + + @Test + mutating func valuePostsAccessSucceededEventWhenFound() async throws { + // set up + let observer = ContextualBusEventObserver(context: ()) + eventBus.addObserver(observer) + + let key = randomConfigKey() + let expectedValue = randomBool() + let variable = ConfigVariable(key: key, defaultValue: !expectedValue) + provider.setValue( + .init( + .bool(expectedValue), + isSecret: randomBool() + ), + forKey: .init(variable.key) + ) + + let (eventStream, continuation) = AsyncStream.makeStream() + observer.addHandler(for: ConfigVariableAccessSucceededEvent.self) { (event, _) in + continuation.yield(event) + } + + // exercise + _ = reader.value(for: variable) + + // expect + let postedEvent = try #require(await eventStream.first { _ in true }) + #expect(postedEvent.key == AbsoluteConfigKey(variable.key)) + #expect(postedEvent.value.content == .bool(expectedValue)) + } +} + + +// MARK: - Primitive TestValues + +% for h in primitive_helpers: +private enum ${h["name"]}TestValues { + static func random(using generator: inout some RandomNumberGenerator) -> ${h["type"]} { + % for line in h["random"]: + ${line} + % end + } + + + static func different(from value: ${h["type"]}, using generator: inout some RandomNumberGenerator) -> ${h["type"]} { + % for line in h["different"]: + ${line} + % end + } + + + static func configContent(for value: ${h["type"]}) -> ConfigContent { + % for line in h["content"]: + ${line} + % end + } +} + + +% end + +// MARK: - MockStringEnum + +private enum MockStringEnum: String, CaseIterable, Sendable { + case alpha + case bravo + case charlie + case delta +} + + +// MARK: - MockConfigStringValue + +private struct MockConfigStringValue: ExpressibleByConfigString, Hashable, Sendable { + let stringValue: String + + var description: String { stringValue } + + init?(configString: String) { + self.stringValue = configString + } +} + + +// MARK: - String-Convertible TestValues + +% for h in string_convertible_helpers: +private enum ${h["name"]}TestValues { + static func random(using generator: inout some RandomNumberGenerator) -> ${h["type"]} { + % for line in h["random"]: + ${line} + % end + } + + + static func different(from value: ${h["type"]}, using generator: inout some RandomNumberGenerator) -> ${h["type"]} { + % for line in h["different"]: + ${line} + % end + } + + + static func configContent(for value: ${h["type"]}) -> ConfigContent { + % for line in h["content"]: + ${line} + % end + } +} + + +% end + +// MARK: - MockIntEnum + +private enum MockIntEnum: Int, CaseIterable, Sendable { + case one = 1 + case two = 2 + case three = 3 + case four = 4 +} + + +// MARK: - MockConfigIntValue + +private struct MockConfigIntValue: ExpressibleByConfigInt, Hashable, Sendable { + let intValue: Int + + var configInt: Int { intValue } + + var description: String { "\(intValue)" } + + init?(configInt: Int) { + self.intValue = configInt + } +} + + +// MARK: - Int-Convertible TestValues + +% for h in int_convertible_helpers: +private enum ${h["name"]}TestValues { + static func random(using generator: inout some RandomNumberGenerator) -> ${h["type"]} { + % for line in h["random"]: + ${line} + % end + } + + + static func different(from value: ${h["type"]}, using generator: inout some RandomNumberGenerator) -> ${h["type"]} { + % for line in h["different"]: + ${line} + % end + } + + + static func configContent(for value: ${h["type"]}) -> ConfigContent { + % for line in h["content"]: + ${line} + % end + } +} + + +% end diff --git a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderTests.swift b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderTests.swift index d5e1347..7d2ea7a 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Core/ConfigVariableReaderTests.swift @@ -2,7 +2,7 @@ // ConfigVariableReaderTests.swift // DevConfiguration // -// Created by Prachi Gauriar on 2/16/2026. +// Created by Prachi Gauriar on 2/16/26. // import Configuration @@ -15,1074 +15,145 @@ import Testing struct ConfigVariableReaderTests: RandomValueGenerating { var randomNumberGenerator = makeRandomNumberGenerator() - /// A mutable provider for testing. - let provider = MutableInMemoryProvider(initialValues: [:]) - /// The event bus for testing event posting. - let eventBus = EventBus() + // MARK: - isSecret - /// The reader under test. - lazy var reader: ConfigVariableReader = { - ConfigVariableReader(providers: [provider], eventBus: eventBus) - }() - - - // MARK: - Bool tests - - @Test - mutating func valueForBoolReturnsProviderValue() { - testValueReturnsProviderValue(using: BoolTestHelper()) - } - - - @Test - mutating func valueForBoolReturnsDefaultWhenKeyNotFound() { - testValueReturnsDefaultWhenKeyNotFound(using: BoolTestHelper()) - } - - - @Test - mutating func fetchValueForBoolReturnsProviderValue() async throws { - try await testFetchValueReturnsProviderValue(using: BoolTestHelper()) - } - - - @Test - mutating func fetchValueForBoolReturnsDefaultWhenKeyNotFound() async throws { - try await testFetchValueReturnsDefaultWhenKeyNotFound(using: BoolTestHelper()) - } - - - @Test - mutating func watchValueForBoolReceivesUpdates() async throws { - try await testWatchValueReceivesUpdates(using: BoolTestHelper()) - } - - - @Test - mutating func subscriptBoolReturnsProviderValue() { - testSubscriptReturnsProviderValue(using: BoolTestHelper()) - } - - - // MARK: - [Bool] tests - - @Test - mutating func valueForBoolArrayReturnsProviderValue() { - testValueReturnsProviderValue(using: BoolArrayTestHelper()) - } - - - @Test - mutating func valueForBoolArrayReturnsDefaultWhenKeyNotFound() { - testValueReturnsDefaultWhenKeyNotFound(using: BoolArrayTestHelper()) - } - - - @Test - mutating func fetchValueForBoolArrayReturnsProviderValue() async throws { - try await testFetchValueReturnsProviderValue(using: BoolArrayTestHelper()) - } - - - @Test - mutating func fetchValueForBoolArrayReturnsDefaultWhenKeyNotFound() async throws { - try await testFetchValueReturnsDefaultWhenKeyNotFound(using: BoolArrayTestHelper()) - } - - - @Test - mutating func watchValueForBoolArrayReceivesUpdates() async throws { - try await testWatchValueReceivesUpdates(using: BoolArrayTestHelper()) - } - - - @Test - mutating func subscriptBoolArrayReturnsProviderValue() { - testSubscriptReturnsProviderValue(using: BoolArrayTestHelper()) - } - - - // MARK: - Float64 tests - - @Test - mutating func valueForFloat64ReturnsProviderValue() { - testValueReturnsProviderValue(using: Float64TestHelper()) - } - - - @Test - mutating func valueForFloat64ReturnsDefaultWhenKeyNotFound() { - testValueReturnsDefaultWhenKeyNotFound(using: Float64TestHelper()) - } - - - @Test - mutating func fetchValueForFloat64ReturnsProviderValue() async throws { - try await testFetchValueReturnsProviderValue(using: Float64TestHelper()) - } - - - @Test - mutating func fetchValueForFloat64ReturnsDefaultWhenKeyNotFound() async throws { - try await testFetchValueReturnsDefaultWhenKeyNotFound(using: Float64TestHelper()) - } - - - @Test - mutating func watchValueForFloat64ReceivesUpdates() async throws { - try await testWatchValueReceivesUpdates(using: Float64TestHelper()) - } - - - @Test - mutating func subscriptFloat64ReturnsProviderValue() { - testSubscriptReturnsProviderValue(using: Float64TestHelper()) - } - - - // MARK: - [Float64] tests - - @Test - mutating func valueForFloat64ArrayReturnsProviderValue() { - testValueReturnsProviderValue(using: Float64ArrayTestHelper()) - } - - - @Test - mutating func valueForFloat64ArrayReturnsDefaultWhenKeyNotFound() { - testValueReturnsDefaultWhenKeyNotFound(using: Float64ArrayTestHelper()) - } - - - @Test - mutating func fetchValueForFloat64ArrayReturnsProviderValue() async throws { - try await testFetchValueReturnsProviderValue(using: Float64ArrayTestHelper()) - } - - - @Test - mutating func fetchValueForFloat64ArrayReturnsDefaultWhenKeyNotFound() async throws { - try await testFetchValueReturnsDefaultWhenKeyNotFound(using: Float64ArrayTestHelper()) - } - - - @Test - mutating func watchValueForFloat64ArrayReceivesUpdates() async throws { - try await testWatchValueReceivesUpdates(using: Float64ArrayTestHelper()) - } - - - @Test - mutating func subscriptFloat64ArrayReturnsProviderValue() { - testSubscriptReturnsProviderValue(using: Float64ArrayTestHelper()) - } - - - // MARK: - Int tests - - @Test - mutating func valueForIntReturnsProviderValue() { - testValueReturnsProviderValue(using: IntTestHelper()) - } - - - @Test - mutating func valueForIntReturnsDefaultWhenKeyNotFound() { - testValueReturnsDefaultWhenKeyNotFound(using: IntTestHelper()) - } - - - @Test - mutating func fetchValueForIntReturnsProviderValue() async throws { - try await testFetchValueReturnsProviderValue(using: IntTestHelper()) - } - - - @Test - mutating func fetchValueForIntReturnsDefaultWhenKeyNotFound() async throws { - try await testFetchValueReturnsDefaultWhenKeyNotFound(using: IntTestHelper()) - } - - - @Test - mutating func watchValueForIntReceivesUpdates() async throws { - try await testWatchValueReceivesUpdates(using: IntTestHelper()) - } - - - @Test - mutating func subscriptIntReturnsProviderValue() { - testSubscriptReturnsProviderValue(using: IntTestHelper()) - } - - - // MARK: - [Int] tests - - @Test - mutating func valueForIntArrayReturnsProviderValue() { - testValueReturnsProviderValue(using: IntArrayTestHelper()) - } - - - @Test - mutating func valueForIntArrayReturnsDefaultWhenKeyNotFound() { - testValueReturnsDefaultWhenKeyNotFound(using: IntArrayTestHelper()) - } - - - @Test - mutating func fetchValueForIntArrayReturnsProviderValue() async throws { - try await testFetchValueReturnsProviderValue(using: IntArrayTestHelper()) - } - - - @Test - mutating func fetchValueForIntArrayReturnsDefaultWhenKeyNotFound() async throws { - try await testFetchValueReturnsDefaultWhenKeyNotFound(using: IntArrayTestHelper()) - } - - - @Test - mutating func watchValueForIntArrayReceivesUpdates() async throws { - try await testWatchValueReceivesUpdates(using: IntArrayTestHelper()) - } - - - @Test - mutating func subscriptIntArrayReturnsProviderValue() { - testSubscriptReturnsProviderValue(using: IntArrayTestHelper()) - } - - - // MARK: - String tests - - @Test - mutating func valueForStringReturnsProviderValue() { - testValueReturnsProviderValue(using: StringTestHelper()) - } - - - @Test - mutating func valueForStringReturnsDefaultWhenKeyNotFound() { - testValueReturnsDefaultWhenKeyNotFound(using: StringTestHelper()) - } - - - @Test - mutating func fetchValueForStringReturnsProviderValue() async throws { - try await testFetchValueReturnsProviderValue(using: StringTestHelper()) - } - - - @Test - mutating func fetchValueForStringReturnsDefaultWhenKeyNotFound() async throws { - try await testFetchValueReturnsDefaultWhenKeyNotFound(using: StringTestHelper()) - } - - - @Test - mutating func watchValueForStringReceivesUpdates() async throws { - try await testWatchValueReceivesUpdates(using: StringTestHelper()) - } - - - @Test - mutating func subscriptStringReturnsProviderValue() { - testSubscriptReturnsProviderValue(using: StringTestHelper()) - } - - - // MARK: - [String] tests - - @Test - mutating func valueForStringArrayReturnsProviderValue() { - testValueReturnsProviderValue(using: StringArrayTestHelper()) - } - - - @Test - mutating func valueForStringArrayReturnsDefaultWhenKeyNotFound() { - testValueReturnsDefaultWhenKeyNotFound(using: StringArrayTestHelper()) - } - - - @Test - mutating func fetchValueForStringArrayReturnsProviderValue() async throws { - try await testFetchValueReturnsProviderValue(using: StringArrayTestHelper()) - } - - - @Test - mutating func fetchValueForStringArrayReturnsDefaultWhenKeyNotFound() async throws { - try await testFetchValueReturnsDefaultWhenKeyNotFound(using: StringArrayTestHelper()) - } - - - @Test - mutating func watchValueForStringArrayReceivesUpdates() async throws { - try await testWatchValueReceivesUpdates(using: StringArrayTestHelper()) - } - - - @Test - mutating func subscriptStringArrayReturnsProviderValue() { - testSubscriptReturnsProviderValue(using: StringArrayTestHelper()) - } - - - // MARK: - [UInt8] tests - - @Test - mutating func valueForBytesReturnsProviderValue() { - testValueReturnsProviderValue(using: BytesTestHelper()) - } - - - @Test - mutating func valueForBytesReturnsDefaultWhenKeyNotFound() { - testValueReturnsDefaultWhenKeyNotFound(using: BytesTestHelper()) - } - - - @Test - mutating func fetchValueForBytesReturnsProviderValue() async throws { - try await testFetchValueReturnsProviderValue(using: BytesTestHelper()) - } - - - @Test - mutating func fetchValueForBytesReturnsDefaultWhenKeyNotFound() async throws { - try await testFetchValueReturnsDefaultWhenKeyNotFound(using: BytesTestHelper()) - } - - - @Test - mutating func watchValueForBytesReceivesUpdates() async throws { - try await testWatchValueReceivesUpdates(using: BytesTestHelper()) - } - - - @Test - mutating func subscriptBytesReturnsProviderValue() { - testSubscriptReturnsProviderValue(using: BytesTestHelper()) - } - - - // MARK: - [[UInt8]] tests - - @Test - mutating func valueForByteChunkArrayReturnsProviderValue() { - testValueReturnsProviderValue(using: ByteChunkArrayTestHelper()) - } - - - @Test - mutating func valueForByteChunkArrayReturnsDefaultWhenKeyNotFound() { - testValueReturnsDefaultWhenKeyNotFound(using: ByteChunkArrayTestHelper()) - } - - - @Test - mutating func fetchValueForByteChunkArrayReturnsProviderValue() async throws { - try await testFetchValueReturnsProviderValue(using: ByteChunkArrayTestHelper()) - } - - - @Test - mutating func fetchValueForByteChunkArrayReturnsDefaultWhenKeyNotFound() async throws { - try await testFetchValueReturnsDefaultWhenKeyNotFound(using: ByteChunkArrayTestHelper()) - } - - - @Test - mutating func watchValueForByteChunkArrayReceivesUpdates() async throws { - try await testWatchValueReceivesUpdates(using: ByteChunkArrayTestHelper()) - } - - - @Test - mutating func subscriptByteChunkArrayReturnsProviderValue() { - testSubscriptReturnsProviderValue(using: ByteChunkArrayTestHelper()) - } - - - // MARK: - Generic Test Helpers - - /// Tests that `value(for:)` returns the provider value when the key exists. - mutating func testValueReturnsProviderValue(using helper: Helper) { - // set up - let key = randomConfigKey() - let expectedValue = helper.randomValue(using: &randomNumberGenerator) - let defaultValue = helper.differentValue(from: expectedValue, using: &randomNumberGenerator) - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - provider.setValue( - .init( - helper.configContent(for: expectedValue), - isSecret: randomBool() - ), - forKey: .init(key) + @Test(arguments: ConfigVariableSecrecy.allCases) + mutating func isSecret(secrecy: ConfigVariableSecrecy) { + let intVariable = ConfigVariable( + key: randomConfigKey(), + defaultValue: randomInt(in: .min ... .max), + secrecy: secrecy ) - // exercise - let result = helper.getValue(from: reader, for: variable) - - // expect - #expect(result == expectedValue) - } - - - /// Tests that `value(for:)` returns the default value when the key is not found. - mutating func testValueReturnsDefaultWhenKeyNotFound(using helper: Helper) { - // set up - let key = randomConfigKey() - let defaultValue = helper.randomValue(using: &randomNumberGenerator) - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - - // exercise - let result = helper.getValue(from: reader, for: variable) - - // expect - #expect(result == defaultValue) - } - - - /// Tests that `fetchValue(for:)` returns the provider value when the key exists. - mutating func testFetchValueReturnsProviderValue( - using helper: Helper - ) async throws { - // set up - let key = randomConfigKey() - let expectedValue = helper.randomValue(using: &randomNumberGenerator) - let defaultValue = helper.differentValue(from: expectedValue, using: &randomNumberGenerator) - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - provider.setValue( - .init( - helper.configContent(for: expectedValue), - isSecret: randomBool() - ), - forKey: .init(key) + let stringVariable = ConfigVariable( + key: randomConfigKey(), + defaultValue: randomAlphanumericString(), + secrecy: secrecy ) - // exercise - let result = try await helper.fetchValue(from: reader, for: variable) - - // expect - #expect(result == expectedValue) - } - - - /// Tests that `fetchValue(for:)` returns the default value when the key is not found. - mutating func testFetchValueReturnsDefaultWhenKeyNotFound( - using helper: Helper - ) async throws { - // set up - let key = randomConfigKey() - let defaultValue = helper.randomValue(using: &randomNumberGenerator) - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - - // exercise - let result = try await helper.fetchValue(from: reader, for: variable) - - // expect - #expect(result == defaultValue) - } - - - /// Tests that `watchValue(for:updatesHandler:)` receives updates when the provider value changes. - mutating func testWatchValueReceivesUpdates( - using helper: Helper - ) async throws { - // set up - let key = randomConfigKey() - let initialValue = helper.randomValue(using: &randomNumberGenerator) - let updatedValue = helper.differentValue(from: initialValue, using: &randomNumberGenerator) - let defaultValue = helper.differentValue(from: initialValue, using: &randomNumberGenerator) - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - provider.setValue( - .init( - helper.configContent(for: initialValue), - isSecret: randomBool() - ), - forKey: .init(key) + let stringArrayVariable = ConfigVariable( + key: randomConfigKey(), + defaultValue: Array(count: randomInt(in: 0 ... 5)) { randomAlphanumericString() }, + secrecy: secrecy ) - // exercise and expect - try await helper.watchValue(from: reader, for: variable) { (updates) in - var iterator = updates.makeAsyncIterator() - - // first value should be initial - let value1 = try await iterator.next() - #expect(value1 == initialValue) - - // update the provider - provider.setValue( - .init( - helper.configContent(for: updatedValue), - isSecret: randomBool() - ), - forKey: .init(key) - ) - - // next value should be updated - let value2 = try await iterator.next() - #expect(value2 == updatedValue) - } - } - - - /// Tests that subscript returns the provider value when the key exists. - mutating func testSubscriptReturnsProviderValue(using helper: Helper) { - // set up - let key = randomConfigKey() - let expectedValue = helper.randomValue(using: &randomNumberGenerator) - let defaultValue = helper.differentValue(from: expectedValue, using: &randomNumberGenerator) - let variable = ConfigVariable(key: key, defaultValue: defaultValue) - provider.setValue( - .init( - helper.configContent(for: expectedValue), - isSecret: randomBool() - ), - forKey: .init(key) + let rawRepresentableStringVariable = ConfigVariable( + key: randomConfigKey(), + defaultValue: MockStringEnum.allCases.randomElement(using: &randomNumberGenerator)!, + secrecy: secrecy ) - // exercise - let result = helper.subscriptValue(from: reader, for: variable) - - // expect - #expect(result == expectedValue) - } - - - // MARK: - Event Bus Integration - - @Test - mutating func valuePostsAccessSucceededEventWhenFound() async throws { - // set up - let observer = ContextualBusEventObserver(context: ()) - eventBus.addObserver(observer) - - let key = randomConfigKey() - let expectedValue = randomBool() - let variable = ConfigVariable(key: key, defaultValue: !expectedValue) - provider.setValue( - .init( - .bool(expectedValue), - isSecret: randomBool() - ), - forKey: .init(variable.key) + let rawRepresentableStringArrayVariable = ConfigVariable( + key: randomConfigKey(), + defaultValue: Array(count: randomInt(in: 0 ... 5)) { + MockStringEnum.allCases.randomElement(using: &randomNumberGenerator)! + }, + secrecy: secrecy ) - let (eventStream, continuation) = AsyncStream.makeStream() - observer.addHandler(for: ConfigVariableAccessSucceededEvent.self) { (event, _) in - continuation.yield(event) - } - - // exercise - _ = reader.value(for: variable) - - // expect - let postedEvent = try #require(await eventStream.first { _ in true }) - #expect(postedEvent.key == AbsoluteConfigKey(variable.key)) - #expect(postedEvent.value.content == .bool(expectedValue)) - } -} - - -// MARK: - ReaderTestHelper Protocol - -/// A protocol that abstracts the type-specific details needed to test `ConfigVariableReader` with different value -/// types. -/// -/// Each conforming type encapsulates random value generation, config content conversion, and reader interaction for a -/// specific value type. -protocol ReaderTestHelper { - /// The configuration value type being tested. - associatedtype Value: Equatable & Sendable - - /// Generates a random value of the associated type. - func randomValue(using generator: inout some RandomNumberGenerator) -> Value - - /// Returns a value that is different from the provided value. - func differentValue(from value: Value, using generator: inout some RandomNumberGenerator) -> Value - - /// Converts the value to its corresponding `ConfigContent` representation. - func configContent(for value: Value) -> ConfigContent - - /// Gets the value from the reader using `value(for:)`. - func getValue(from reader: ConfigVariableReader, for variable: ConfigVariable) -> Value - - /// Fetches the value from the reader using `fetchValue(for:)`. - func fetchValue(from reader: ConfigVariableReader, for variable: ConfigVariable) async throws -> Value - - /// Watches the value from the reader using `watchValue(for:updatesHandler:)`. - func watchValue( - from reader: ConfigVariableReader, - for variable: ConfigVariable, - updatesHandler: (ConfigUpdatesAsyncSequence) async throws -> Return - ) async throws -> Return - - /// Gets the value from the reader using the subscript. - func subscriptValue(from reader: ConfigVariableReader, for variable: ConfigVariable) -> Value -} - - -// MARK: - BoolTestHelper - -private struct BoolTestHelper: ReaderTestHelper { - func randomValue(using generator: inout some RandomNumberGenerator) -> Bool { - Bool.random(using: &generator) - } - - - func differentValue(from value: Bool, using generator: inout some RandomNumberGenerator) -> Bool { - !value - } - - - func configContent(for value: Bool) -> ConfigContent { - .bool(value) - } - - - func getValue(from reader: ConfigVariableReader, for variable: ConfigVariable) -> Bool { - reader.value(for: variable) - } - - - func fetchValue(from reader: ConfigVariableReader, for variable: ConfigVariable) async throws -> Bool { - try await reader.fetchValue(for: variable) - } - - - func watchValue( - from reader: ConfigVariableReader, - for variable: ConfigVariable, - updatesHandler: (ConfigUpdatesAsyncSequence) async throws -> Return - ) async throws -> Return { - try await reader.watchValue(for: variable, updatesHandler: updatesHandler) - } - - - func subscriptValue(from reader: ConfigVariableReader, for variable: ConfigVariable) -> Bool { - reader[variable] - } -} - - -// MARK: - Float64TestHelper - -private struct Float64TestHelper: ReaderTestHelper { - func randomValue(using generator: inout some RandomNumberGenerator) -> Float64 { - Float64.random(in: 1 ... 100_000, using: &generator) - } - - - func differentValue(from value: Float64, using generator: inout some RandomNumberGenerator) -> Float64 { - value + randomValue(using: &generator) - } - - - func configContent(for value: Float64) -> ConfigContent { - .double(value) - } - - - func getValue(from reader: ConfigVariableReader, for variable: ConfigVariable) -> Float64 { - reader.value(for: variable) - } - - - func fetchValue( - from reader: ConfigVariableReader, - for variable: ConfigVariable - ) async throws -> Float64 { - try await reader.fetchValue(for: variable) - } - - - func watchValue( - from reader: ConfigVariableReader, - for variable: ConfigVariable, - updatesHandler: (ConfigUpdatesAsyncSequence) async throws -> Return - ) async throws -> Return { - try await reader.watchValue(for: variable, updatesHandler: updatesHandler) - } - - - func subscriptValue(from reader: ConfigVariableReader, for variable: ConfigVariable) -> Float64 { - reader[variable] - } -} - - -// MARK: - IntTestHelper - -private struct IntTestHelper: ReaderTestHelper { - func randomValue(using generator: inout some RandomNumberGenerator) -> Int { - Int.random(in: 1 ... 100_000, using: &generator) - } - - - func differentValue(from value: Int, using generator: inout some RandomNumberGenerator) -> Int { - value + randomValue(using: &generator) - } - - - func configContent(for value: Int) -> ConfigContent { - .int(value) - } - - - func getValue(from reader: ConfigVariableReader, for variable: ConfigVariable) -> Int { - reader.value(for: variable) - } - - - func fetchValue(from reader: ConfigVariableReader, for variable: ConfigVariable) async throws -> Int { - try await reader.fetchValue(for: variable) - } - - - func watchValue( - from reader: ConfigVariableReader, - for variable: ConfigVariable, - updatesHandler: (ConfigUpdatesAsyncSequence) async throws -> Return - ) async throws -> Return { - try await reader.watchValue(for: variable, updatesHandler: updatesHandler) - } - - - func subscriptValue(from reader: ConfigVariableReader, for variable: ConfigVariable) -> Int { - reader[variable] - } -} - - -// MARK: - StringTestHelper - -private struct StringTestHelper: ReaderTestHelper { - func randomValue(using generator: inout some RandomNumberGenerator) -> String { - let count = Int.random(in: 5 ... 20, using: &generator) - return String.randomAlphanumeric(count: count, using: &generator) - } - - - func differentValue(from value: String, using generator: inout some RandomNumberGenerator) -> String { - value + randomValue(using: &generator) - } - - - func configContent(for value: String) -> ConfigContent { - .string(value) - } - - - func getValue(from reader: ConfigVariableReader, for variable: ConfigVariable) -> String { - reader.value(for: variable) - } - - - func fetchValue(from reader: ConfigVariableReader, for variable: ConfigVariable) async throws -> String { - try await reader.fetchValue(for: variable) - } - - - func watchValue( - from reader: ConfigVariableReader, - for variable: ConfigVariable, - updatesHandler: (ConfigUpdatesAsyncSequence) async throws -> Return - ) async throws -> Return { - try await reader.watchValue(for: variable, updatesHandler: updatesHandler) - } - - - func subscriptValue(from reader: ConfigVariableReader, for variable: ConfigVariable) -> String { - reader[variable] - } -} - - -// MARK: - BytesTestHelper - -private struct BytesTestHelper: ReaderTestHelper { - func randomValue(using generator: inout some RandomNumberGenerator) -> [UInt8] { - let count = Int.random(in: 1 ... 32, using: &generator) - return Array(count: count) { UInt8.random(in: .min ... .max, using: &generator) } - } - - - func differentValue(from value: [UInt8], using generator: inout some RandomNumberGenerator) -> [UInt8] { - value + randomValue(using: &generator) - } - - - func configContent(for value: [UInt8]) -> ConfigContent { - .bytes(value) - } - - - func getValue(from reader: ConfigVariableReader, for variable: ConfigVariable<[UInt8]>) -> [UInt8] { - reader.value(for: variable) - } + let expressibleByConfigStringVariable = ConfigVariable( + key: randomConfigKey(), + defaultValue: MockConfigStringValue(configString: randomAlphanumericString())!, + secrecy: secrecy + ) + let expressibleByConfigStringArrayVariable = ConfigVariable( + key: randomConfigKey(), + defaultValue: Array(count: randomInt(in: 0 ... 5)) { + MockConfigStringValue(configString: randomAlphanumericString())! + }, + secrecy: secrecy + ) - func fetchValue( - from reader: ConfigVariableReader, - for variable: ConfigVariable<[UInt8]> - ) async throws -> [UInt8] { - try await reader.fetchValue(for: variable) - } + let rawRepresentableIntVariable = ConfigVariable( + key: randomConfigKey(), + defaultValue: MockIntEnum.allCases.randomElement(using: &randomNumberGenerator)!, + secrecy: secrecy + ) + let rawRepresentableIntArrayVariable = ConfigVariable( + key: randomConfigKey(), + defaultValue: Array(count: randomInt(in: 0 ... 5)) { + MockIntEnum.allCases.randomElement(using: &randomNumberGenerator)! + }, + secrecy: secrecy + ) - func watchValue( - from reader: ConfigVariableReader, - for variable: ConfigVariable<[UInt8]>, - updatesHandler: (ConfigUpdatesAsyncSequence<[UInt8], Never>) async throws -> Return - ) async throws -> Return { - try await reader.watchValue(for: variable, updatesHandler: updatesHandler) - } + let expressibleByConfigIntVariable = ConfigVariable( + key: randomConfigKey(), + defaultValue: MockConfigIntValue(configInt: randomInt(in: .min ... .max))!, + secrecy: secrecy + ) + let expressibleByConfigIntArrayVariable = ConfigVariable( + key: randomConfigKey(), + defaultValue: Array(count: randomInt(in: 0 ... 5)) { + MockConfigIntValue(configInt: randomInt(in: .min ... .max))! + }, + secrecy: secrecy + ) - func subscriptValue(from reader: ConfigVariableReader, for variable: ConfigVariable<[UInt8]>) -> [UInt8] { - reader[variable] + let isNotPublic = [.secret, .auto].contains(secrecy) + let isSecret = secrecy == .secret + + let reader = ConfigVariableReader(providers: [InMemoryProvider(values: [:])], eventBus: EventBus()) + #expect(reader.isSecret(intVariable) == isSecret) + #expect(reader.isSecret(stringVariable) == isNotPublic) + #expect(reader.isSecret(stringArrayVariable) == isNotPublic) + #expect(reader.isSecret(rawRepresentableStringVariable) == isNotPublic) + #expect(reader.isSecret(rawRepresentableStringArrayVariable) == isNotPublic) + #expect(reader.isSecret(expressibleByConfigStringVariable) == isNotPublic) + #expect(reader.isSecret(expressibleByConfigStringArrayVariable) == isNotPublic) + #expect(reader.isSecret(rawRepresentableIntVariable) == isSecret) + #expect(reader.isSecret(rawRepresentableIntArrayVariable) == isSecret) + #expect(reader.isSecret(expressibleByConfigIntVariable) == isSecret) + #expect(reader.isSecret(expressibleByConfigIntArrayVariable) == isSecret) } } -// MARK: - BoolArrayTestHelper - -private struct BoolArrayTestHelper: ReaderTestHelper { - func randomValue(using generator: inout some RandomNumberGenerator) -> [Bool] { - let count = Int.random(in: 1 ... 5, using: &generator) - return Array(count: count) { Bool.random(using: &generator) } - } - - - func differentValue(from value: [Bool], using generator: inout some RandomNumberGenerator) -> [Bool] { - value + randomValue(using: &generator) - } - - - func configContent(for value: [Bool]) -> ConfigContent { - .boolArray(value) - } - - - func getValue(from reader: ConfigVariableReader, for variable: ConfigVariable<[Bool]>) -> [Bool] { - reader.value(for: variable) - } - - - func fetchValue( - from reader: ConfigVariableReader, - for variable: ConfigVariable<[Bool]> - ) async throws -> [Bool] { - try await reader.fetchValue(for: variable) - } - - - func watchValue( - from reader: ConfigVariableReader, - for variable: ConfigVariable<[Bool]>, - updatesHandler: (ConfigUpdatesAsyncSequence<[Bool], Never>) async throws -> Return - ) async throws -> Return { - try await reader.watchValue(for: variable, updatesHandler: updatesHandler) - } +// MARK: - MockStringEnum - - func subscriptValue(from reader: ConfigVariableReader, for variable: ConfigVariable<[Bool]>) -> [Bool] { - reader[variable] - } +private enum MockStringEnum: String, CaseIterable, Sendable { + case alpha + case bravo + case charlie } -// MARK: - Float64ArrayTestHelper - -private struct Float64ArrayTestHelper: ReaderTestHelper { - func randomValue(using generator: inout some RandomNumberGenerator) -> [Float64] { - let count = Int.random(in: 1 ... 5, using: &generator) - return Array(count: count) { Float64.random(in: 1 ... 100_000, using: &generator) } - } - - - func differentValue(from value: [Float64], using generator: inout some RandomNumberGenerator) -> [Float64] { - value + randomValue(using: &generator) - } - - - func configContent(for value: [Float64]) -> ConfigContent { - .doubleArray(value) - } - - - func getValue(from reader: ConfigVariableReader, for variable: ConfigVariable<[Float64]>) -> [Float64] { - reader.value(for: variable) - } - +// MARK: - MockConfigStringValue - func fetchValue( - from reader: ConfigVariableReader, - for variable: ConfigVariable<[Float64]> - ) async throws -> [Float64] { - try await reader.fetchValue(for: variable) - } - - - func watchValue( - from reader: ConfigVariableReader, - for variable: ConfigVariable<[Float64]>, - updatesHandler: (ConfigUpdatesAsyncSequence<[Float64], Never>) async throws -> Return - ) async throws -> Return { - try await reader.watchValue(for: variable, updatesHandler: updatesHandler) - } +private struct MockConfigStringValue: ExpressibleByConfigString, Hashable, Sendable { + let stringValue: String + var description: String { stringValue } - func subscriptValue(from reader: ConfigVariableReader, for variable: ConfigVariable<[Float64]>) -> [Float64] { - reader[variable] + init?(configString: String) { + self.stringValue = configString } } -// MARK: - IntArrayTestHelper - -private struct IntArrayTestHelper: ReaderTestHelper { - func randomValue(using generator: inout some RandomNumberGenerator) -> [Int] { - let count = Int.random(in: 1 ... 5, using: &generator) - return Array(count: count) { Int.random(in: 1 ... 100_000, using: &generator) } - } - +// MARK: - MockIntEnum - func differentValue(from value: [Int], using generator: inout some RandomNumberGenerator) -> [Int] { - value + randomValue(using: &generator) - } - - - func configContent(for value: [Int]) -> ConfigContent { - .intArray(value) - } - - - func getValue(from reader: ConfigVariableReader, for variable: ConfigVariable<[Int]>) -> [Int] { - reader.value(for: variable) - } - - - func fetchValue( - from reader: ConfigVariableReader, - for variable: ConfigVariable<[Int]> - ) async throws -> [Int] { - try await reader.fetchValue(for: variable) - } - - - func watchValue( - from reader: ConfigVariableReader, - for variable: ConfigVariable<[Int]>, - updatesHandler: (ConfigUpdatesAsyncSequence<[Int], Never>) async throws -> Return - ) async throws -> Return { - try await reader.watchValue(for: variable, updatesHandler: updatesHandler) - } - - - func subscriptValue(from reader: ConfigVariableReader, for variable: ConfigVariable<[Int]>) -> [Int] { - reader[variable] - } +private enum MockIntEnum: Int, CaseIterable, Sendable { + case one = 1 + case two = 2 + case three = 3 + case four = 4 } -// MARK: - StringArrayTestHelper - -private struct StringArrayTestHelper: ReaderTestHelper { - func randomValue(using generator: inout some RandomNumberGenerator) -> [String] { - let count = Int.random(in: 1 ... 5, using: &generator) - return Array(count: count) { String.randomAlphanumeric(count: count * 3, using: &generator) } - } - - - func differentValue(from value: [String], using generator: inout some RandomNumberGenerator) -> [String] { - value + randomValue(using: &generator) - } - - - func configContent(for value: [String]) -> ConfigContent { - .stringArray(value) - } - - - func getValue(from reader: ConfigVariableReader, for variable: ConfigVariable<[String]>) -> [String] { - reader.value(for: variable) - } - +// MARK: - MockConfigIntValue - func fetchValue( - from reader: ConfigVariableReader, - for variable: ConfigVariable<[String]> - ) async throws -> [String] { - try await reader.fetchValue(for: variable) - } - - - func watchValue( - from reader: ConfigVariableReader, - for variable: ConfigVariable<[String]>, - updatesHandler: (ConfigUpdatesAsyncSequence<[String], Never>) async throws -> Return - ) async throws -> Return { - try await reader.watchValue(for: variable, updatesHandler: updatesHandler) - } - - - func subscriptValue(from reader: ConfigVariableReader, for variable: ConfigVariable<[String]>) -> [String] { - reader[variable] - } -} - - -// MARK: - ByteChunkArrayTestHelper - -private struct ByteChunkArrayTestHelper: ReaderTestHelper { - func randomValue(using generator: inout some RandomNumberGenerator) -> [[UInt8]] { - let count = Int.random(in: 1 ... 5, using: &generator) - return Array(count: count) { - let byteCount = Int.random(in: 1 ... 32, using: &generator) - return Array(count: byteCount) { UInt8.random(in: .min ... .max, using: &generator) } - } - } - - - func differentValue(from value: [[UInt8]], using generator: inout some RandomNumberGenerator) -> [[UInt8]] { - value + randomValue(using: &generator) - } - - - func configContent(for value: [[UInt8]]) -> ConfigContent { - .byteChunkArray(value) - } - - - func getValue(from reader: ConfigVariableReader, for variable: ConfigVariable<[[UInt8]]>) -> [[UInt8]] { - reader.value(for: variable) - } - - - func fetchValue( - from reader: ConfigVariableReader, - for variable: ConfigVariable<[[UInt8]]> - ) async throws -> [[UInt8]] { - try await reader.fetchValue(for: variable) - } - - - func watchValue( - from reader: ConfigVariableReader, - for variable: ConfigVariable<[[UInt8]]>, - updatesHandler: (ConfigUpdatesAsyncSequence<[[UInt8]], Never>) async throws -> Return - ) async throws -> Return { - try await reader.watchValue(for: variable, updatesHandler: updatesHandler) - } +private struct MockConfigIntValue: ExpressibleByConfigInt, Hashable, Sendable { + let intValue: Int + var configInt: Int { intValue } + var description: String { "\(intValue)" } - func subscriptValue(from reader: ConfigVariableReader, for variable: ConfigVariable<[[UInt8]]>) -> [[UInt8]] { - reader[variable] + init?(configInt: Int) { + self.intValue = configInt } }