Wrapper APIs
Gitar can be tailored to user-defined feature flag APIs or SDK wrappers via a
TOML
configuration file.
The configuration files are stored under the .gitar/spec/
folder at the root
of the repository. Each configuration file contains the wrapper api
specification for a programming language.
Supported patterns
The feature flag configurations support common feature flag API coding patterns:
-
Wrapper functions: User-defined functions or methods that take a flag key and return a flag value, commonly used to wrap vendor APIs. These are configured in the
[[usage]]
and[[usage.api]]
TOML tables. -
Wrapper types: Flag values wrapped in lambdas, tuples, or objects. Codebases often define custom wrapper functions and types to more easily follow language, framework, or team conventions related to error handling, concurrency, API composition, and other patterns. The Types section below specifies these wrapper types.
-
Flag aliases: Flag keys aliased by identifiers that are enum elements, constants, or instances of other custom types, commonly used to add a layer of compile-time safety. While Gitar can automatically detect aliasing when a variable is explicitly assigned a literal string, code generation tools make this aliasing invisible as generated code is usually not checked into a repo. These aliases are configured in
[[definition]]
TOML tables. -
Code generation: Code-generated functions (methods) that access flags. Codebases often define accessor functions that return the value of a specific flag to hide boilerplate involved in calling lower-level SDKs. While Gitar can automatically detect and clean up these functions when they are present in the repo, code generation tools make these accessor functions invisible as generated code is usually not checked into a repo. These are defined in
[[code_gen]]
TOML tables. -
Testing mocks: Mock functions that enable setting flag values for testing. These are defined in
[[test_usage]]
and[[test_usage.api]]
TOML tables.
Each TOML table has an optional examples
key with an array of code examples
that documents the coding patterns that the table supports. This is purely for
documentation purposes to illustrate the patterns they intend to capture.
Meta variables
Meta-variables are identifiers that start with '@'
and used in the type
specifications to bind to return values, arguments, and aliases. They identify
flag keys and values in API signatures and can also be used to chain API
functions together.
<meta-var> ::= '@' <identifier>
Note that <identifier>
cannot start with an underscore character (’_’).
There are 2 pre-defined meta-variables that have special meaning:
@flag_key
identifies a feature flag key.@flag_value
identifies a feature flag value.
A single flag cleanup operation in Gitar first looks for instances of
@flag_key
matching the specific flag name to be cleaned. It then replaces
instances of feature flag’s @flag_value
with the now constant value of the
flag following knowledge of the feature flagging SDK in use and the
configuration in these specifications. It finally simplifies the code further.
Type specifications
The following grammar defines types. At the top-level, there are base, tuple, and function types:
<type> ::= <base-type> | <tuple-type> | <fun-type>
A meta-variable can be bound to a type to create a bound type:
<bound-type> ::= ( <meta-var> ':' )? <type>
Base types define language-specific primitive types:
<base-type> ::= 'bool' | 'string' | 'number' | 'object' | 'void' | '_'
'_'
means any type and used when the specific type doesn’t matter.
For example,
@flag_key:string
means a flag key of type string@flag_value:_
means a flag value that can have any type or whose type doesn’t matter.@my_ff:object
means a user-defined meta-variable that has an object type.@my_val:_
means a user-defined meta-variable that can have any type or whose type doesn’t matter.
Tuple types are un-named aggregrate types:
<tuple-type> :: = '(' ( <bound-type> ( ',' <bound-type>+ )? )? ')'
Tuples are language-specific types typically used in language-specific
error-handling and destructuring patterns; for example, (bool,_)
:
- Go:
(bool, _)
- Python:
Tuple[bool, _]
. - Rust:
Result<bool, _>
.
Function types are signatures for lambdas:
<fun-type> ::= <tuple-type> '->' <bound-type>
Function types are typically used for passing around lambda functions that return flag values when invoked; for example,
() -> @flag_value:bool
returns a boolean flag value when invoked(@flag_key:string) -> @flag_value:bool
returns a boolean flag value when invoked with a string key(@flag_value:_) -> void
a function that takes the value of the flag (where we don’t care about the type) and returns nothing; for example, a callback that is invoked to observe the value of a flag
[[usage]]
[[usage.api]]
[[usage.api]]
defines the signature of a function (or method) that takes a
meta-variable (e.g., flag key or alias) or returns a meta-variable (e.g., flag
value). The signature is defined by the function (method) name, its argument
array, a receiver (for methods), and a return type.
[[usage.api]]
also defines accesses to named fields within objects or structs
that contain flag values.
The [[usage.api]]
table must have exactly one of the following two keys:
api
: The name of a wrapper function or method.accessor
: The name of an object or struct field that contains a flag value.
Wrapper functions and field accessors can have the following optional keys:
receiver
: A<bound-type>
with a<meta-var>
that is the receiver object (e.g., thethis
reference in Java) defined in areturn
key of another[[usage.api]]
or an alias defined in aname
key of a[[definition]]
. This key is optional forapi
but mandatory foraccessor
.return
: A<type>
representing the return type. This key is optional and defaults to'@flag_value:_'
if not present.
Wrapper functions can have the following optional key:
args
: An array of<bound-type>
representing the function signature. This key is optional and defaults to['@flag_key:string']
if not present.
[[usage.api]]api = "getFlagValue"examples = [''' // Go const flagValue = getFlagValue("flag_key");''']
[[usage.api]]api = 'variation'args = ['@flag_key:string', '_', '_', '(@flag_value:bool, _) -> _']return = '_'examples = [''' // JS // Observe the value of 'my-flag-key-123abc' ldClient.variation('my-flag-key-123abc', context, false, (flagValue, err) => { // Code using the feature flag value here! });''']
[[usage.api]]api = 'makeFlagCallback'args = ['@flag_key:string', '_']return = "() -> @flag_value:bool"examples = [''' // Go cb := makeFlagCallback("other-123abc", false); if cb() { // Feature is enabled: gated code here } // Alternatively: flagValue := cb() // Use flagValue normally''']
[[usage.api]]api = 'getFlagObject'args = ['@flag_key:string', '_']return = '@flag_obj:object'examples = [''' // Go flagWrapperObj := getFlagObject("flag-key", false)''']
[[usage.api]]api = 'get'args = ['_']receiver = '@flag_obj:object'examples = [''' // Go flagValue := getFlagObject("flag-key", false).get(ctx)''']
[[usage.api]]api = 'tryGet'args = ['_']receiver = '@flag_obj:object'return = '(@flag_value:bool, _)'examples = [''' // Go flagValue, err := getFlagObject("flag-key", false).tryGet(ctx)''']
[[usage.api]]api = "getFlag"args = ['@my_ff:object'] # from the [[definition]] (see below) matching object MyFeatureFlag : ...return = '@flag_info:object'examples = [''' // Kotlin object MyFeatureFlag : Flag( name = "flag_key", )
// Access through a flag client val featureFlagsClient: FeatureFlagsClient
// Observe the value featureFlagsClient.getFlag(MyFeatureFlag).observe { flagValue -> // do something that checks for flagValue }''']
[[usage.api]]accessor = "val"receiver = '@flag_info:object'examples = [''' // Kotlin // Get the current value directly val flagValue = featureFlagsClient.getFlag(MyFeatureFlag).val''']
[[usage.api]]api = "observe"receiver = '@flag_info:object'args = ['(@flag_value:_) -> void']return = 'void'examples = [''' // Kotlin // Observe the value featureFlagsClient.getFlag(MyFeatureFlag).observe { flagValue -> // do something that checks for flagValue }''']
[[definition]]
The [[definition]]
table defines a regular expression pattern to capture
identifiers that alias flag keys.
This table has two keys:
name
: A unique name that can be used to reference this alias as a meta-variable.pattern
: A regular expression that captures the alias for the flag key.
[[definition]]name = "flag_key_definition"pattern = '''(\w+) .* `ld:'FLAG_KEY''' # regular expressionexamples = [ ''' type FlagValues struct { Key123abc bool `ld:'flag-key-123abc, false'` } ''']
[[definition]]name = "my_ff"pattern = '''object (\w+) : Flag\((\n|.)*?name = 'FLAG_KEY'(\n|.)*\)'''examples = [ ''' // Kotlin object MyFeatureFlag : Flag( name = "flag_key", ) ''']
[[code_gen]]
The [[code_gen]]
table defines custom API calls that return flag values. These
APIs are typically created by code generation tools that generate accessor
functions or methods that return flag values and whose names are based on flag
keys or their aliases.
[[code_gen]]
can include the following optional keys:
applies_to
: A list of names of flag key aliases defined asname
s in[[definition]]
s. This key is optional and defaults to all defined flag aliases.operation
: A transformation applying to the alias above to produce the name of the generated symbol. It defaults to the identity transformation (i.e. alias"name"
becomes generated methodname()
). Other options include adding a suffix or prefix, or changing the case (e.g.CamelCase
orlowercase
)return
: A<type>
representing the return type. It defaults to'@flag_value:_'
if not present.
[[definition]]name = "flag_key_definition" # an id used to reference this definitionpattern = '''(\w+) .* `ld:'FLAG_KEY''' # regular expressionexamples = [ ''' // Go type FlagValues struct { Key123abc bool `ld:'flag-key-123abc, false'` } ''']
[[code_gen]]return = "() -> @flag_value:_"examples = [''' // Go flagValue := ctx.Flags.Key123abc()''']
[[code_gen]]operation = "suffix:Noisy"applies_to = ['flag_key_definition']return = "() -> (@flag_value:_, _)"examples = [''' // Go flagValue, err := ctx.Flags.Key123abcNoisy()''']
[[test_usage]]
[[test_usage.api]]
[[test_usage.api]]
defines the signature of a mock function used to set the
value of a experiment flag for testing.
The [[test_usage.api]]
table must have the following key:
api
: The name of the flag mock function or method.
It also can have the following optional keys:
receiver
: A<bound-type>
with a<meta-var>
that is the receiver object (e.g., thethis
reference in Java) defined in areturn
key of another[[test_usage.api]]
or[usage.api]
, or an alias defined in aname
key of a[[definition]]
.return
: A<type>
representing the return type. It defaults to'void'
if not present.args
: An array of<bound-type>
representing the function signature. It defaults to['@flag_key:string', '@flag_value:bool']
if not present, meaning the method takes the feature flag name and the value it should be set to for the rest of the test.
[[test_usages.apis]]api = 'setupLDMock'examples = [''' // JS setupLDMock('flag-key', true)''']
Similar to [[usage.api]]
, [test_usage.api]
functions can be composed and
chained through intermediate objects captured as <meta-var>
s returned from one
api and used as the receiver or argument of another. Note that while
[test_usage.api]
tables can refer to <meta-var>
s defined in [[usage.api]]
tables, the reverse is not true.