Skip to content

Latest commit

 

History

History
305 lines (212 loc) · 12.1 KB

introduction.md

File metadata and controls

305 lines (212 loc) · 12.1 KB

↑ DOCUMENTATION INDEX

1. Introduction

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:

@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:

@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

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

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:

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:

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

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:

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

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.

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:

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

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:

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:

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:

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:

@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

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:

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

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 FlagsCollections and use the setDefaultValue() on each different flag to setup your own value.

Consider this example:

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

If you need to reset the custom value set for a flag inside each of its set provider you can use resetValue():

try? loader.$flagA.resetValue() 

This remove any custom value set for this flag in each of its writable provider.

↑ INDEX

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