Skip to content

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., the this reference in Java) defined in a return key of another [[usage.api]] or an alias defined in a name key of a [[definition]]. This key is optional for api but mandatory for accessor.
  • 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 expression
examples = [
'''
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 as names 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 method name()). Other options include adding a suffix or prefix, or changing the case (e.g. CamelCase or lowercase)
  • 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 definition
pattern = '''(\w+) .* `ld:'FLAG_KEY''' # regular expression
examples = [
'''
// 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., the this reference in Java) defined in a return key of another [[test_usage.api]] or [usage.api], or an alias defined in a name 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.