Skip to content

Commit

Permalink
feat: add toggle flag step (#111)
Browse files Browse the repository at this point in the history
* add accessToken/baseUri to flagModel

* refactor BuilToggleFlagPatch to flags client

* add tests for toggle flag command

* send patch when toggling flag

* use consts for default proj/env keys

* pr feedback
  • Loading branch information
k3llymariee authored Apr 3, 2024
1 parent 4e08a1c commit 9cd4018
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 34 deletions.
10 changes: 2 additions & 8 deletions cmd/flags/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,8 @@ func runUpdate(client flags.Client) func(*cobra.Command, []string) error {
var patch []flags.UpdateInput
if cmd.CalledAs() == "toggle-on" || cmd.CalledAs() == "toggle-off" {
_ = viper.BindPFlag(cliflags.EnvironmentFlag, cmd.Flags().Lookup(cliflags.EnvironmentFlag))
err := json.Unmarshal([]byte(buildPatch(viper.GetString(cliflags.EnvironmentFlag), cmd.CalledAs() == "toggle-on")), &patch)
if err != nil {
return err
}
envKey := viper.GetString(cliflags.EnvironmentFlag)
patch = flags.BuildToggleFlagPatch(envKey, cmd.CalledAs() == "toggle-on")
} else {
err := json.Unmarshal([]byte(viper.GetString(cliflags.DataFlag)), &patch)
if err != nil {
Expand All @@ -151,7 +149,3 @@ func runUpdate(client flags.Client) func(*cobra.Command, []string) error {
return nil
}
}

func buildPatch(envKey string, toggleValue bool) string {
return fmt.Sprintf(`[{"op": "replace", "path": "/environments/%s/on", "value": %t}]`, envKey, toggleValue)
}
80 changes: 80 additions & 0 deletions cmd/flags/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,83 @@ func TestUpdate(t *testing.T) {
assert.EqualError(t, err, "base-uri is invalid"+errorHelp)
})
}

func TestToggle(t *testing.T) {
errorHelp := ". See `ldcli flags toggle-on --help` for supported flags and usage."
mockArgs := []interface{}{
"testAccessToken",
"http://test.com",
"test-proj-key",
"test-flag-key",
[]flags.UpdateInput{
{
Op: "replace",
Path: "/environments/test-env-key/on",
Value: true,
},
},
}
t.Run("with valid flags calls projects API", func(t *testing.T) {
client := flags.MockClient{}
client.
On("Update", mockArgs...).
Return([]byte(cmd.ValidResponse), nil)
args := []string{
"flags", "toggle-on",
"--access-token", "testAccessToken",
"--base-uri", "http://test.com",
"--flag", "test-flag-key",
"--project", "test-proj-key",
"--environment", "test-env-key",
}

output, err := cmd.CallCmd(t, nil, &client, nil, nil, args)

require.NoError(t, err)
assert.JSONEq(t, `{"valid": true}`, string(output))
})

t.Run("with an error response is an error", func(t *testing.T) {
client := flags.MockClient{}
client.
On("Update", mockArgs...).
Return([]byte(`{}`), errors.NewError("An error"))
args := []string{
"flags", "toggle-on",
"--access-token", "testAccessToken",
"--base-uri", "http://test.com",
"--flag", "test-flag-key",
"--project", "test-proj-key",
"--environment", "test-env-key",
}

_, err := cmd.CallCmd(t, nil, &client, nil, nil, args)

require.EqualError(t, err, "An error")
})

t.Run("with missing required flags is an error", func(t *testing.T) {
args := []string{
"flags", "toggle-on",
}

_, err := cmd.CallCmd(t, nil, &flags.MockClient{}, nil, nil, args)

assert.EqualError(t, err, `required flag(s) "access-token", "environment", "flag", "project" not set`+errorHelp)
})

t.Run("with invalid base-uri is an error", func(t *testing.T) {
args := []string{
"flags", "toggle-on",
"--access-token", "testAccessToken",
"--base-uri", "invalid",
"--flag", "test-flag-key",
"--project", "test-proj-key",
"--environment", "test-env-key",
}

_, err := cmd.CallCmd(t, nil, &flags.MockClient{}, nil, nil, args)

assert.EqualError(t, err, "base-uri is invalid"+errorHelp)
})
}
5 changes: 5 additions & 0 deletions internal/flags/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package flags
import (
"context"
"encoding/json"
"fmt"

ldapi "github.com/launchdarkly/api-client-go/v14"

Expand Down Expand Up @@ -92,3 +93,7 @@ func (c FlagsClient) Update(

return responseJSON, nil
}

func BuildToggleFlagPatch(envKey string, enabled bool) []UpdateInput {
return []UpdateInput{{Op: "replace", Path: fmt.Sprintf("/environments/%s/on", envKey), Value: enabled}}
}
17 changes: 12 additions & 5 deletions internal/quickstart/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import (
"ldcli/internal/flags"
)

const (
defaultProjKey = "default"
defaultEnvKey = "test"
)

// ContainerModel is a high level container model that controls the nested models wher each
// represents a step in the quick-start flow.
type ContainerModel struct {
Expand All @@ -23,13 +28,14 @@ type ContainerModel struct {
accessToken string
baseUri string
currentModel tea.Model
sdkKind string
}

func NewContainerModel(flagsClient flags.Client, accessToken string, baseUri string) tea.Model {
return ContainerModel{
accessToken: accessToken,
baseUri: baseUri,
currentModel: NewCreateFlagModel(flagsClient),
currentModel: NewCreateFlagModel(flagsClient, accessToken, baseUri),
flagsClient: flagsClient,
}
}
Expand All @@ -53,18 +59,19 @@ func (m ContainerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case choseSDKMsg:
m.currentModel = NewShowSDKInstructionsModel(m.accessToken, m.baseUri, msg.canonicalName, msg.url, m.flagKey)
cmd = m.currentModel.Init()
m.sdkKind = msg.sdkKind
case createdFlagMsg:
m.currentModel = NewChooseSDKModel()
m.flagKey = msg.flagKey // TODO: figure out if we maintain state here or pass in another message
case errMsg:
m.err = msg.err
case noInstructionsMsg:
// TODO: set currentModel to toggle flag model
// m.currentModel = NewToggleFlagModel(m.flagsClient, m.flagKey)
case fetchedSDKInstructions, fetchedEnv:
// skip the ShowSDKInstructionsModel and move along to toggling the flag
m.currentModel = NewToggleFlagModel(m.flagsClient, m.accessToken, m.baseUri, m.flagKey, m.sdkKind)
case fetchedSDKInstructions, fetchedEnv, toggledFlagMsg:
m.currentModel, cmd = m.currentModel.Update(msg)
case showToggleFlagMsg:
m.currentModel = NewToggleFlagModel(m.flagKey)
m.currentModel = NewToggleFlagModel(m.flagsClient, m.accessToken, m.baseUri, m.flagKey, m.sdkKind)
default:
log.Println("container default - bad", msg)
}
Expand Down
23 changes: 10 additions & 13 deletions internal/quickstart/create_flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ package quickstart

import (
"fmt"
"ldcli/cmd/cliflags"

"github.com/spf13/viper"

"ldcli/internal/flags"

"github.com/charmbracelet/bubbles/key"
Expand All @@ -17,20 +13,24 @@ import (
const defaultFlagName = "my new flag"

type createFlagModel struct {
client flags.Client
textInput textinput.Model
accessToken string
baseUri string
client flags.Client
textInput textinput.Model
}

func NewCreateFlagModel(client flags.Client) tea.Model {
func NewCreateFlagModel(client flags.Client, accessToken, baseUri string) tea.Model {
ti := textinput.New()
ti.Focus()
ti.CharLimit = 156
ti.Width = 20
ti.Placeholder = defaultFlagName

return createFlagModel{
client: client,
textInput: ti,
accessToken: accessToken,
baseUri: baseUri,
client: client,
textInput: ti,
}
}

Expand All @@ -53,10 +53,7 @@ func (m createFlagModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, sendErr(err)
}

accessToken := viper.GetString(cliflags.AccessTokenFlag)
baseUri := viper.GetString(cliflags.BaseURIFlag)

return m, sendCreateFlagMsg(m.client, accessToken, baseUri, input, flagKey, "default")
return m, sendCreateFlagMsg(m.client, m.accessToken, m.baseUri, input, flagKey, defaultProjKey)
case key.Matches(msg, keys.Quit):
return m, tea.Quit
default:
Expand Down
21 changes: 21 additions & 0 deletions internal/quickstart/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,25 @@ func sendErr(err error) tea.Cmd {
}
}

type toggledFlagMsg struct{}

func sendToggleFlagMsg(client flags.Client, accessToken, baseUri, flagKey string, enabled bool) tea.Cmd {
return func() tea.Msg {
_, err := client.Update(
context.Background(),
accessToken,
baseUri,
flagKey,
defaultProjKey,
flags.BuildToggleFlagPatch(defaultEnvKey, enabled),
)
if err != nil {
return sendErr(err)
}
return toggledFlagMsg{}
}
}

type createdFlagMsg struct {
flagKey string
}
Expand Down Expand Up @@ -75,6 +94,7 @@ type fetchedSDKInstructions struct {
type choseSDKMsg struct {
canonicalName string
displayName string
sdkKind string
url string
}

Expand All @@ -88,6 +108,7 @@ func sendChoseSDKMsg(sdk sdkDetail) tea.Cmd {
canonicalName: sdk.canonicalName,
displayName: sdk.displayName,
url: sdk.url,
sdkKind: sdk.kind,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/quickstart/show_sdk_instructions.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func NewShowSDKInstructionsModel(
func (m showSDKInstructionsModel) Init() tea.Cmd {
return tea.Sequence(
sendFetchSDKInstructionsMsg(m.url),
sendFetchEnv(m.accessToken, m.baseUri, "test", "default"),
sendFetchEnv(m.accessToken, m.baseUri, defaultEnvKey, defaultProjKey),
)
}

Expand Down
47 changes: 40 additions & 7 deletions internal/quickstart/toggle_flag.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
package quickstart

import (
"fmt"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"ldcli/internal/flags"
)

type toggleFlagModel struct {
flagKey string
accessToken string
baseUri string
client flags.Client
enabled bool
flagKey string
flagWasEnabled bool
sdkKind string
}

func NewToggleFlagModel(flagKey string) tea.Model {
func NewToggleFlagModel(client flags.Client, accessToken string, baseUri string, flagKey string, sdkKind string) tea.Model {
return toggleFlagModel{
flagKey: flagKey,
accessToken: accessToken,
baseUri: baseUri,
client: client,
flagKey: flagKey,
sdkKind: sdkKind,
}
}

Expand All @@ -25,22 +37,43 @@ func (m toggleFlagModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, keys.Quit):
return m, tea.Quit
case key.Matches(msg, keys.Tab):
// TODO: toggle flag
m.flagWasEnabled = true
m.enabled = !m.enabled
return m, sendToggleFlagMsg(m.client, m.accessToken, m.baseUri, m.flagKey, m.enabled)
}
}

return m, cmd
}

var logTypeMap = map[string]string{
serverSideSDK: "application logs",
clientSideSDK: "browser",
}

func (m toggleFlagModel) View() string {
var furtherInstructions string
title := "Toggle your feature flag (press tab)"
toggle := "OFF"
bgColor := "#646a73"
margin := 1
if m.enabled {
bgColor = "#3d9c51"
margin = 2
toggle = "ON"
}

if m.flagWasEnabled {
furtherInstructions = fmt.Sprintf("\n\nCheck your %s to see the change!\n\n(press ctrl + c to quit)", logTypeMap[m.sdkKind])
}

toggleStyle := lipgloss.NewStyle().
Background(lipgloss.Color("#646a73")).
Background(lipgloss.Color(bgColor)).
Padding(0, 1).
MarginRight(1)
MarginRight(margin)

return title + "\n\n" + toggleStyle.Render(toggle) + m.flagKey
return title + "\n\n" + toggleStyle.Render(toggle) + m.flagKey + furtherInstructions
}

0 comments on commit 9cd4018

Please sign in to comment.