fakemonster/elm-codegen-feature-flags/1.0.0

elm-codegen-feature-flags

An Elm code generation library built on top of elm-codegen to help you generate feature flags without any boilerplate.

By "feature flags", we're talking about a record with a bunch of primitive types, mostly booleans. Something like:

type alias FeatureFlags =
  { animateSignInButton : Bool
  , useNewFooter : Bool
  }

With minimal config, this package aims to make it easy (and safe!) to maintain, extend, and reduce these records without thinking hard (or, ideally, at all).

Table of contents

Installation

Like any Elm package, you're going to need Elm, and a project to install stuff in! After that:

  1. Install elm-codegen
  2. Set up a Generate.elm file (elm-codegen CLI will do this for you automatically)
  3. Within your codegen/ directory: elm install fakemonster/elm-codegen-feature-flags

Why use feature flags?

Let's say you're working on a big new feature for your website. You've already written a ton of code, and you'd like to deploy it and let people take a look, but it's not ready for the general public yet.

A common convention is to use "feature flags": runtime configuration (usually passed as url parameters or json objects!) that you key off of to activate your alternative experience. They're usually pretty simple, and I often see them implemented as a big record of primitive values. Pretty much always booleans, sometimes a string for using a new API version or something.

What's hard about them?

Over time, especially on a big project, these records can get unwieldy:

type alias FeatureFlags =
    { customApiDomain : Maybe String
    , eagerLoadInvites : Bool
    , largeLoginButton : Bool
    , largeSignupButton : Bool
    , useExperimentalAnimationLibrary : Bool
    -- we won't add a sixth one. right?
    }

And they can become error-prone when you've got to serialize and deserialize them, or you copy-paste a bit too much adding a new one:

decoder =
    map5
        (field "customApiDomain" (JD.maybe JD.string))
        (field "eagerLoadInvites" JD.bool)
        (field "largeSignupButton" JD.bool)
        (field "largeLoginButton" JD.bool) -- wrong order!!
        (field "experimentalAnimationLibary" JD.bool) -- typo!

Now, in our applications we have ways of preventing many of these issues. We could use custom types instead of booleans, or tag all of them with phantom types to avoid mixups. We could use functions or clever type aliases to get the compiler to guide us through the decoder, encoder etc. We could write tests! And I strongly encourage you to do all that in the interesting parts of your application.

But feature flags aren't interesting! They're a common pattern, it's a pile of data for powering if expressions, and the only thing custom to you is the field name, at the end of the day. So why not codegen them?

What this library generates

Straightforwardly, this is a codegen module that can make you a big (or small!) record full of Bools, Strings, and Maybe Strings. They'll be bundled with:

  1. a JSON encoder and decoder pair (if you call withJsonConverters)
  2. a URL builder and parser pair (if you call withUrlConverters)
  3. utility functions for easy handling, including joining multiple flag sets and iterating over them

Used in elm-codegen, generally your code will look something like this:

module Generate exposing (main)

import Gen.CodeGen.Generate as Generate
import CodeGen.FeatureFlags as FF


main : Program {} () ()
main =
    Generate.run
        [ FF.fromFlags
            [ ( "customApiDomain", FF.maybeString )
            , ( "welcomeMessage", FF.string "Welcome!" )
            , ( "eagerLoadInvites", FF.bool )
            , ( "largeLoginButton", FF.bool )
            , ( "largeSignupButton", FF.bool )
            , ( "useExperimentalAnimationLibrary", FF.bool )
            ]
            |> FF.withJsonConverters
            |> FF.withUrlConverters
            |> FF.withQueryKeyFormatter (\s -> "f-" ++ s)
            |> FF.generate "MyFeatureFlags"
        ]

This generates a module named MyFeatureFlags with:

  • a FeatureFlags alias containing your given fields and types
  • JSON and URL querystring encoders and decoders
  • a setter for each field
  • utility functions and types to do common operations safely

Then you can use it wherever you like:

init : JD.Value -> ( Model, Cmd Msg )
init flags =
    ( { flags =
            Json.Decode.decodeValue MyFeatureFlags.decoder flags
                |> Result.withDefault MyFeatureFlags.default
      }
    , Cmd.none
    )

view : Model -> Html Msg
view model =
    div []
        [ if model.flags.largeSignupButton then
            button [ HE.onClick ClickedSignup, HA.class "button-large" ]
                [ text "PLEASE click!" ]

          else
            button [ HE.onClick ClickedSignup, HA.class "button" ]
                [ text "Click here!" ]
        ]

If you're looking with a careful eye, what this package really does is automatic derivation for JSON serialization, URL serialization, and a couple of custom concepts of "merging" (or) and "iterating" (WhenApplied x), all limited to "records with a few primitive types".

Following along

The examples directory shows an application using FeatureFlags in practice. Clone this repo, and then in the examples directory:

  1. npx elm-codegen install
  2. npx elm-codegen run --output generated

You can also use elm-doc-preview to see documentation for the generated code as well! All these commands are in the Makefile as well.

Feedback

The code generated here is based on my experience with feature flags, and the types of transformation and maintenance that I've had to do with them. If you have other utilities you'd be interested in, or support for additional types, please open an issue to let me know.