[↑ DOCUMENTATION INDEX](./../README.md#documentation) # 1. Introduction - 1.1 [@Flag Annotation](#11-flag-annotation) - 1.2 [@Flag Supported Datatypes](#12--flag-supported-data-types) - 1.3 [`Codable` types and `@Flag`](#13-codable-types-and--flag) - 1.4 [Computed @Flag](#14-computed--flag) - 1.5 [Load a Feature Flag Collection in a `FlagLoader`](#15-load-a-feature-flag-collection-in-a-flagloader) - 1.6 [Configure Key Evaluation for `FlagsLoader`'s `@Flag`](#16-configure-key-evaluation-for-flagsloaders-flag) - 1.7 [Query a specific data provider](#17-query-a-specific-data-provider) - 1.8 [Set Flag defaultValue at runtime](#18-set-flag-defaultvalue-at-runtime) - 1.9 [Reset Flag values](#19-reset-flag-values) - 1.10 [Reset LocalProvider values](#110-reset-localprovider-values) ## 1.1 `@Flag` Annotation To create a feature flag you must create a `FlagProtocol` conform object and use the `@Flag` property wrapper annotation. Here some examples of feature flags: ```swift @Flag(name: "Awesome Property", key: "my_awesome_prop", default: 0, excludedProviders: [FirebaseProvider.self], description: "This is a description of the property" ) var awesomeProperty: Int ``` As you can see the `@Flag` annotation allows you to define the following properties (don't worry, excluding `description` all of them are optionals): - `name`: Identify the name of the property. If not set the property name is used automatically. This value is used by the Flags Browser. - `key`: The key which identify this property to the data providers. If not specified the value is automatically generated by the tree structure using the string transformation assigned to the `FlagLoader` instance *(for example if this property is inside the `MyGroup` the default full key will be `my_group.awesome_property`). - `default`: the default fallback value is key's value is not returned by any of the specified `FlagsLoader`'s data providers. - `excludedProviders`: you can also exclude some data providers of the `FlagLoader` instance which load this property by adding their types here. For example if you add `[FirebaseProvider.type]`, the Firebase Remote Config service is never used when asked for this ff's value. - `description`: This is the only required parameter: it shortly describe what the flag is for. This is used by Flag Browser for providing context for the flags you are enabling/disabling in the UI, but it also provides context for future developers. Most of the time you will need just set the description and the key of the feature flag, like: ```swift @Flag(key: "ios_app_rating_mode", description: "What kind of popup to show") var ratingMode: String? ``` The following `ratingMode` describe an optional `String` feature flag. When loaded into an instance the loader itself ask the value `ios_app_rating_mode` to any specified data provider. If no value is found the `default` option is returned (in this case, as optional, it just return `nil`). [↑ INDEX](#introduction) ## 1.2 `@Flag` Supported Data Types RealFlags allows you to define your own feature flags; it supports any primitive type both in wrapped and optional form: - `Bool` - `Int` & `UInt` (in 8, 16, 32 and 64 variants) - `String` - `Data` - `Date` - `URL` - `Dictionary` and `Array` where value is any object conform to `FlagProtocol` - `JSON` via `JSONData` custom type [↑ INDEX](#introduction) ## 1.3 Codable types and @Flag When you need to support a custom datatype you just need to make it conform to the `Codable` protocol and `FlagProtocol`; serialization/deserialization operations are performed automatically by the library. This is an example with `CLLocationCoordinate2D` which by default is not conform to `Codable` protocol: ```swift extension CLLocationCoordinate2D: FlagProtocol, Codable { public func encode(to encoder: Encoder) throws { var container = encoder.unkeyedContainer() try container.encode(longitude) try container.encode(latitude) } public init(from decoder: Decoder) throws { var container = try decoder.unkeyedContainer() let longitude = try container.decode(CLLocationDegrees.self) let latitude = try container.decode(CLLocationDegrees.self) self.init(latitude: latitude, longitude: longitude) } } ``` Once you made it conform you are able to just use easily: ```swift public struct MapFlags: FlagCollectionProtocol { @Flag(default: CLLocationCoordinate2D(latitude: 33, longitude: 33), description: "...") var defaultCoordinates: CLLocationCoordinate2D public init() { } } ``` Moreover all `Codable` ready object are automatically conforms to `FlagProtocol` so you can virtually use any object type as feature flag. ### NOTE While you can define virtually any kind of data type as feature flag using the `@Flag` annotation you must keep in mind not all data providers may supports them. [↑ INDEX](#introduction) ## 1.4 Computed @Flag Sometimes you may need to create a feature flag where the value is a combination of other flags or runtime values you need to evaluate dynamically. This is the perfect example to use the `computedValue` of the `@Flag` annotation. `computedValue` allows you to define a callback function which is called before any other defined provider. When the function return a non `nil` value it will be the value of the flag (and no further checks are made on providers). If you return a `nil` value the library will perform a default check to defined providers and default value. ### NOTE `computedValue` should be short but you may encounter situations where the value must be a bit complex to evaluate. In this case **we strongly suggest defining a `private static func` in your struct and refer it into the flag definition. See the code below. The following example defines a `Bool` property `hasPublishButton` where the value is returned by checking the current language of the app: ```swift public struct MiscFlags: FlagCollectionProtocol { // MARK: - Flags @Flag(default: false, computedValue: MiscFlags.computedPublishButton, description: "") var hasPublishButton: Bool // MARK: - Computed Properties Functions public init() { } private static func computedPublishButton() -> Bool? { Language.main.code == "it" } } ``` [↑ INDEX](#introduction) ## 1.5 Load a Feature Flag Collection in a `FlagLoader` `@Flag` allows you to describe a feature flag property. However you can consider it as description of the property structure. Value for a feature flag is obtained by an instance of a `FlagLoader`. `FlagLoader` load a structure and a list of data providers. ```swift let firebase = FirebaseRemoteProvider() let local = LocalProvider(localURL: localFile) let appFlags = FlagsLoader(AppFeatureFlags.self, providers: [local, firebase]) ``` `appFlags` load the `AppFeatureFlag` ff collection and use `[local, firebase]` as ordered data providers. This mean that when you ask for a value inside, ie: ```swift let currentValue = appFlags.ratingMode ``` RealFlags ask for value in the following order: - to `local`, *if no value is returned then ask to* - to `firebase` *if no value is returned then* - fallback `default` value [↑ INDEX](#introduction) ## 1.6 Configure Key Evaluation for `FlagsLoader`'s `@Flag` `FlagLoader` instance can be initialized by also configuring how keys are evaluated for each `@Flag` of the loaded collection. Each `@Flag` annotated property uses the automatic key evaluation; the key used to query a data provider is extracted from the position of the property in the nested structure (if any) plus the name of the variable itself. Consider the following structure: ```swift struct Flags: FlagCollectionProtocol { @FlagCollection(description: "...") var nested: NestedCollection @Flag(default: false, description: "...") var flatBoolean: Bool } struct NestedCollection: FlagCollectionProtocol { @Flag(default: false, description: "...") var nestedBoolean: Bool } ``` You have two properties: - `flatBoolean` is in the root structure - `nestedBoolean` is inside the `NestedCollection` By default the key which RealFlags uses to query for values to any set data provider are: - `flat_boolean` for `flatBoolean` property - `nested/nested_boolean` for `nestedBoolean` property You can print them, once loaded using: ```swift print("flatBoolean key: \(loader.$flatBoolean.keyPath.fullPath))") // flat_boolean print("nestedBoolean key: \(loader.nested.$nestedBoolean.keyPath.fullPath))") // nested/nested_boolean ``` You can configure how keys are evaluated by passing a `KeyConfiguration` when initializing the `FlagsLoader`: ```swift let config = KeyConfiguration(prefix: "myapp_", pathSeparator: ".", keyTransform: .snakeCase) let loader = FlagsManager(providers: [...], keyConfiguration: config) ``` A `KeyConfiguration` defines: - `prefix`: the prefix to append at the start of each evaluated key (in this case it will be `myapp/flat_boolean` and `myapp/nested/nested_boolean`) - `pathSeparator`: separator to use for each path component; by default is `/` but you can choose, for example `.`. - `keyTransform`: how the property name/collection name must be transformed. You can choose between `none` (no transformation, `flatBoolean` property's key still `flatBoolean`), `kebabCase` (it will be `flatBoolean`) or `snakeCase` (it will be `flat_boolean`). If you don't want to use the automatic key evaluation you can set the fixed `key` value of the `@Flag`: ```swift @Flag(key: "nestedAwesomeProp", default: false, description: "...") var nestedBoolean: Bool ``` In this case the automatic key evaluation is disabled for this property and `nestedAwesomeProp` is the value to query to any data provider. [↑ INDEX](#introduction) ## 1.7 Query a specific data provider Sometimes you may need to query a specific provider for a value. Consider the previous property and suppose you want query just the `Firebase` provider: ```swift let valueInFirebase = appFlags.$ratingMode.flagValue(from: FirebaseRemoteProvider.self) ``` The `flagValue()` function (accessible via the `$` of the property wrapper) allows you to specify a particular data provider to query. > NOTE: If provider is not previously set into the `FlagsLoader` the fallback value is returned instead. [↑ INDEX](#introduction) ## 1.8 [Set Flag defaultValue at runtime](#18-set-flag-defaultvalue-at-runtime) Sometimes you may want to alter the default value of a `Flag` set via the annotation `default` parameters. This is true when, for example, you have different target of your product with different values for some flag and you would avoid creating duplicate files for each Flags Collection blue print. In this case you can define your own `FlagsCollection`s and use the `setDefaultValue()` on each different flag to setup your own value. Consider this example: ```swift struct Flags: FlagCollectionProtocol { @FlagCollection(default: 100, description: "...") var flagA: Int @Flag(default: false, description: "...") var flagB: Bool } public func setupFlagsByTarget { self.loader = FlagsLoader(Flags.self, provider: [...]) #if TARGET_A // Target A only differ for a 200 default value for flagA loader.$flagA.setDefault(200) #endif #if TARGET_B // Target B only differ in flagB which is false by default loader.$flagB.setDefault(false) #endif #if TARGET_C // Target C has the same false for flagB but a different value for flagA loader.$flagA.setDefault(50) #endif } ``` [↑ INDEX](#introduction) ## 1.9 [Reset Flag values](#18-reset-flag-values) If you need to reset the custom value set for a flag inside each of its set provider you can use `resetValue()`: ```swift try? loader.$flagA.resetValue() ``` This remove any custom value set for this flag in each of its **writable provider**. [↑ INDEX](#introduction) ## 1.10 [Reset LocalProvider values](#18-reset-localprovider-values) You can also reset an entire `LocalProvider` instance optionally backed by a disk file. Just call `resetAllData()` on your instance and both in-memory and disk value (when set) will be removed from the provider. [↑ INDEX](#introduction)