diff --git a/cmd/flags/update.go b/cmd/flags/update.go index e789ccdf..21775144 100644 --- a/cmd/flags/update.go +++ b/cmd/flags/update.go @@ -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 { @@ -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) -} diff --git a/cmd/flags/update_test.go b/cmd/flags/update_test.go index 9508fe79..1c30e29f 100644 --- a/cmd/flags/update_test.go +++ b/cmd/flags/update_test.go @@ -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) + }) +} diff --git a/internal/flags/client.go b/internal/flags/client.go index d3de7de7..2a46ab18 100644 --- a/internal/flags/client.go +++ b/internal/flags/client.go @@ -3,6 +3,7 @@ package flags import ( "context" "encoding/json" + "fmt" ldapi "github.com/launchdarkly/api-client-go/v14" @@ -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}} +} diff --git a/internal/quickstart/container.go b/internal/quickstart/container.go index 8726ff86..d0042151 100644 --- a/internal/quickstart/container.go +++ b/internal/quickstart/container.go @@ -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 { @@ -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, } } @@ -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) } diff --git a/internal/quickstart/create_flag.go b/internal/quickstart/create_flag.go index 6f71211e..db34269f 100644 --- a/internal/quickstart/create_flag.go +++ b/internal/quickstart/create_flag.go @@ -2,10 +2,6 @@ package quickstart import ( "fmt" - "ldcli/cmd/cliflags" - - "github.com/spf13/viper" - "ldcli/internal/flags" "github.com/charmbracelet/bubbles/key" @@ -17,11 +13,13 @@ 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 @@ -29,8 +27,10 @@ func NewCreateFlagModel(client flags.Client) tea.Model { ti.Placeholder = defaultFlagName return createFlagModel{ - client: client, - textInput: ti, + accessToken: accessToken, + baseUri: baseUri, + client: client, + textInput: ti, } } @@ -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: diff --git a/internal/quickstart/messages.go b/internal/quickstart/messages.go index c911ab56..7c6c6425 100644 --- a/internal/quickstart/messages.go +++ b/internal/quickstart/messages.go @@ -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 } @@ -75,6 +94,7 @@ type fetchedSDKInstructions struct { type choseSDKMsg struct { canonicalName string displayName string + sdkKind string url string } @@ -88,6 +108,7 @@ func sendChoseSDKMsg(sdk sdkDetail) tea.Cmd { canonicalName: sdk.canonicalName, displayName: sdk.displayName, url: sdk.url, + sdkKind: sdk.kind, } } } diff --git a/internal/quickstart/show_sdk_instructions.go b/internal/quickstart/show_sdk_instructions.go index 116977a5..4cd0afe7 100644 --- a/internal/quickstart/show_sdk_instructions.go +++ b/internal/quickstart/show_sdk_instructions.go @@ -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), ) } diff --git a/internal/quickstart/toggle_flag.go b/internal/quickstart/toggle_flag.go index 39caaeb5..ac1e5cee 100644 --- a/internal/quickstart/toggle_flag.go +++ b/internal/quickstart/toggle_flag.go @@ -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, } } @@ -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 }