From 1e3ee7b19c62141e93d964b29bd73903cd916204 Mon Sep 17 00:00:00 2001 From: Danny Olson Date: Thu, 28 Mar 2024 11:54:37 -0700 Subject: [PATCH 1/7] Create choose SDK step --- internal/quickstart/choose_sdk.go | 32 ++++++++++++++++++++++++++++++ internal/quickstart/container.go | 6 +++++- internal/quickstart/create_flag.go | 2 +- 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 internal/quickstart/choose_sdk.go diff --git a/internal/quickstart/choose_sdk.go b/internal/quickstart/choose_sdk.go new file mode 100644 index 00000000..41af2a78 --- /dev/null +++ b/internal/quickstart/choose_sdk.go @@ -0,0 +1,32 @@ +package quickstart + +import ( + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" +) + +type chooseSDKModel struct{} + +func NewChooseSDKModel() tea.Model { + return chooseSDKModel{} +} + +func (p chooseSDKModel) Init() tea.Cmd { + return nil +} + +func (m chooseSDKModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, keys.Enter): + } + } + + return m, cmd +} + +func (m chooseSDKModel) View() string { + return "Choose SDK" +} diff --git a/internal/quickstart/container.go b/internal/quickstart/container.go index 865dfb32..054477a9 100644 --- a/internal/quickstart/container.go +++ b/internal/quickstart/container.go @@ -15,6 +15,7 @@ type step int const ( createFlagStep step = iota + chooseSDKStep ) // ContainerModel is a high level container model that controls the nested models wher each @@ -34,6 +35,7 @@ func NewContainerModel(flagsClient flags.Client) tea.Model { flagsClient: flagsClient, steps: []tea.Model{ NewCreateFlagModel(flagsClient), + NewChooseSDKModel(), }, } } @@ -59,6 +61,8 @@ func (m ContainerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.flagKey = model.flagKey m.currentStep += 1 } + case chooseSDKStep: + m.currentStep += 1 default: } case key.Matches(msg, keys.Quit): @@ -90,7 +94,7 @@ func (m ContainerModel) View() string { } // TODO: remove after creating more steps - if m.currentStep > createFlagStep { + if m.currentStep > chooseSDKStep { return fmt.Sprintf("created flag %s", m.flagKey) } diff --git a/internal/quickstart/create_flag.go b/internal/quickstart/create_flag.go index 1b0ece0d..43ba6cce 100644 --- a/internal/quickstart/create_flag.go +++ b/internal/quickstart/create_flag.go @@ -37,7 +37,7 @@ func NewCreateFlagModel(client flags.Client) tea.Model { } } -func (p createFlagModel) Init() tea.Cmd { +func (m createFlagModel) Init() tea.Cmd { return nil } From 21bf5e713bdc1f02437750d508570466bd6aea43 Mon Sep 17 00:00:00 2001 From: Danny Olson Date: Thu, 28 Mar 2024 13:50:15 -0700 Subject: [PATCH 2/7] Add list of SDKs --- internal/quickstart/choose_sdk.go | 106 +++++++++++++++++++++++++++++- internal/quickstart/container.go | 15 ++++- 2 files changed, 115 insertions(+), 6 deletions(-) diff --git a/internal/quickstart/choose_sdk.go b/internal/quickstart/choose_sdk.go index 41af2a78..ecbb1022 100644 --- a/internal/quickstart/choose_sdk.go +++ b/internal/quickstart/choose_sdk.go @@ -1,14 +1,68 @@ package quickstart import ( + "fmt" + "io" + "strings" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +var ( + sdkStyle = lipgloss.NewStyle().PaddingLeft(4) + selectedSdkItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) ) -type chooseSDKModel struct{} +type chooseSDKModel struct { + choice sdk + list list.Model +} func NewChooseSDKModel() tea.Model { - return chooseSDKModel{} + sdks := []sdk{ + { + name: "JavaScript", + }, + { + name: "Node.js (server)", + }, + { + name: "Python", + }, + { + name: "Java", + }, + { + name: "Android", + }, + { + name: "React Native", + }, + { + name: "Ruby", + }, + { + name: "Flutter", + }, + } + + l := list.New(sdksToItems(sdks), sdkDelegate{}, 30, 14) + // extra newlines to show pagination + l.Title = "Select your SDK:\n\n" + // reset title styles + l.Styles.Title = lipgloss.NewStyle() + l.Styles.TitleBar = lipgloss.NewStyle() + l.SetShowPagination(true) + l.SetShowStatusBar(false) + l.SetFilteringEnabled(false) + l.Paginator.PerPage = 5 + + return chooseSDKModel{ + list: l, + } } func (p chooseSDKModel) Init() tea.Cmd { @@ -21,6 +75,14 @@ func (m chooseSDKModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: switch { case key.Matches(msg, keys.Enter): + i, ok := m.list.SelectedItem().(sdk) + if ok { + m.choice = i + } + case key.Matches(msg, keys.Quit): + return m, tea.Quit + default: + m.list, cmd = m.list.Update(msg) } } @@ -28,5 +90,43 @@ func (m chooseSDKModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m chooseSDKModel) View() string { - return "Choose SDK" + return m.list.View() +} + +type sdk struct { + name string +} + +func (s sdk) FilterValue() string { return "" } + +type sdkDelegate struct{} + +func (d sdkDelegate) Height() int { return 1 } +func (d sdkDelegate) Spacing() int { return 0 } +func (d sdkDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } +func (d sdkDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + i, ok := listItem.(sdk) + if !ok { + return + } + + str := fmt.Sprintf("%d. %s", index+1, i.name) + + fn := sdkStyle.Render + if index == m.Index() { + fn = func(s ...string) string { + return selectedSdkItemStyle.Render("> " + strings.Join(s, " ")) + } + } + + fmt.Fprint(w, fn(str)) +} + +func sdksToItems(sdks []sdk) []list.Item { + items := make([]list.Item, len(sdks)) + for i, sdk := range sdks { + items[i] = list.Item(sdk) + } + + return items } diff --git a/internal/quickstart/container.go b/internal/quickstart/container.go index 054477a9..48fdfbb8 100644 --- a/internal/quickstart/container.go +++ b/internal/quickstart/container.go @@ -26,6 +26,7 @@ type ContainerModel struct { flagKey string flagsClient flags.Client quitting bool + sdk sdk steps []tea.Model } @@ -62,7 +63,13 @@ func (m ContainerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.currentStep += 1 } case chooseSDKStep: - m.currentStep += 1 + updated, cmd := m.steps[chooseSDKStep].Update(msg) + if model, ok := updated.(chooseSDKModel); ok { + m.sdk = model.choice + m.currentStep += 1 + } + + return m, cmd default: } case key.Matches(msg, keys.Quit): @@ -71,8 +78,10 @@ func (m ContainerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit default: // delegate all other input to the current model - updated, _ := m.steps[m.currentStep].Update(msg) + updated, cmd := m.steps[m.currentStep].Update(msg) m.steps[m.currentStep] = updated + + return m, cmd } default: } @@ -95,7 +104,7 @@ func (m ContainerModel) View() string { // TODO: remove after creating more steps if m.currentStep > chooseSDKStep { - return fmt.Sprintf("created flag %s", m.flagKey) + return fmt.Sprintf("created flag %s with SDK %s", m.flagKey, m.sdk.name) } return fmt.Sprintf("\nStep %d of %d\n"+m.steps[m.currentStep].View(), m.currentStep+1, len(m.steps)) From dbed28c692c71603353ba69ef18d404abc632eff Mon Sep 17 00:00:00 2001 From: Danny Olson Date: Fri, 29 Mar 2024 09:38:08 -0700 Subject: [PATCH 3/7] Fetch and show SDK instructions --- internal/quickstart/choose_sdk.go | 29 ++++--- internal/quickstart/container.go | 43 +++++++--- internal/quickstart/messages.go | 18 ++++ internal/quickstart/show_sdk_instructions.go | 89 ++++++++++++++++++++ 4 files changed, 159 insertions(+), 20 deletions(-) create mode 100644 internal/quickstart/messages.go create mode 100644 internal/quickstart/show_sdk_instructions.go diff --git a/internal/quickstart/choose_sdk.go b/internal/quickstart/choose_sdk.go index ecbb1022..0f69fa46 100644 --- a/internal/quickstart/choose_sdk.go +++ b/internal/quickstart/choose_sdk.go @@ -24,28 +24,36 @@ type chooseSDKModel struct { func NewChooseSDKModel() tea.Model { sdks := []sdk{ { - name: "JavaScript", + canonicalName: "js", + name: "JavaScript", }, { - name: "Node.js (server)", + canonicalName: "node-server", + name: "Node.js (server)", }, { - name: "Python", + canonicalName: "python", + name: "Python", }, { - name: "Java", + canonicalName: "java", + name: "Java", }, { - name: "Android", + canonicalName: "android", + name: "Android", }, { - name: "React Native", + canonicalName: "react-native", + name: "React Native", }, { - name: "Ruby", + canonicalName: "ruby", + name: "Ruby", }, { - name: "Flutter", + canonicalName: "flutter", + name: "Flutter", }, } @@ -65,7 +73,7 @@ func NewChooseSDKModel() tea.Model { } } -func (p chooseSDKModel) Init() tea.Cmd { +func (m chooseSDKModel) Init() tea.Cmd { return nil } @@ -94,7 +102,8 @@ func (m chooseSDKModel) View() string { } type sdk struct { - name string + canonicalName string + name string } func (s sdk) FilterValue() string { return "" } diff --git a/internal/quickstart/container.go b/internal/quickstart/container.go index 48fdfbb8..a0637bfd 100644 --- a/internal/quickstart/container.go +++ b/internal/quickstart/container.go @@ -16,6 +16,7 @@ type step int const ( createFlagStep step = iota chooseSDKStep + showSDKInstructionsStep ) // ContainerModel is a high level container model that controls the nested models wher each @@ -37,6 +38,7 @@ func NewContainerModel(flagsClient flags.Client) tea.Model { steps: []tea.Model{ NewCreateFlagModel(flagsClient), NewChooseSDKModel(), + NewShowSDKInstructionsModel(), }, } } @@ -46,36 +48,43 @@ func (m ContainerModel) Init() tea.Cmd { } func (m ContainerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var ( + cmd tea.Cmd + updated tea.Model + ) switch msg := msg.(type) { case tea.KeyMsg: switch { + case key.Matches(msg, keys.Quit): + m.quitting = true + + return m, tea.Quit case key.Matches(msg, keys.Enter): switch m.currentStep { case createFlagStep: - updated, _ := m.steps[createFlagStep].Update(msg) + updated, cmd = m.steps[createFlagStep].Update(msg) if model, ok := updated.(createFlagModel); ok { if model.err != nil { m.err = model.err - - return m, nil } m.flagKey = model.flagKey m.currentStep += 1 } case chooseSDKStep: - updated, cmd := m.steps[chooseSDKStep].Update(msg) + updated, cmd = m.steps[chooseSDKStep].Update(msg) if model, ok := updated.(chooseSDKModel); ok { m.sdk = model.choice m.currentStep += 1 } + case showSDKInstructionsStep: + updated, cmd := m.steps[showSDKInstructionsStep].Update(msg) + if _, ok := updated.(showSDKInstructionsModel); ok { + m.currentStep += 1 + } return m, cmd default: } - case key.Matches(msg, keys.Quit): - m.quitting = true - - return m, tea.Quit default: // delegate all other input to the current model updated, cmd := m.steps[m.currentStep].Update(msg) @@ -83,10 +92,24 @@ func (m ContainerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } + switch m.currentStep { + case showSDKInstructionsStep: + updated, cmd = m.steps[showSDKInstructionsStep].Update(fetchSDKInstructionsMsg{ + canonicalName: m.sdk.canonicalName, + name: m.sdk.name, + }) + if model, ok := updated.(showSDKInstructionsModel); ok { + model.sdk = m.sdk.name + m.steps[showSDKInstructionsStep] = model + } + default: + } + case errMsg: + m.err = msg.err default: } - return m, nil + return m, cmd } func (m ContainerModel) View() string { @@ -103,7 +126,7 @@ func (m ContainerModel) View() string { } // TODO: remove after creating more steps - if m.currentStep > chooseSDKStep { + if m.currentStep > showSDKInstructionsStep { return fmt.Sprintf("created flag %s with SDK %s", m.flagKey, m.sdk.name) } diff --git a/internal/quickstart/messages.go b/internal/quickstart/messages.go new file mode 100644 index 00000000..b25dccc6 --- /dev/null +++ b/internal/quickstart/messages.go @@ -0,0 +1,18 @@ +package quickstart + +import tea "github.com/charmbracelet/bubbletea" + +type fetchSDKInstructionsMsg struct { + canonicalName string + name string +} + +type errMsg struct { + err error +} + +func sendErr(err error) tea.Cmd { + return func() tea.Msg { + return errMsg{err: err} + } +} diff --git a/internal/quickstart/show_sdk_instructions.go b/internal/quickstart/show_sdk_instructions.go new file mode 100644 index 00000000..3593d315 --- /dev/null +++ b/internal/quickstart/show_sdk_instructions.go @@ -0,0 +1,89 @@ +package quickstart + +import ( + "fmt" + "io" + "ldcli/internal/errors" + "net/http" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/glamour" + "github.com/charmbracelet/lipgloss" + "github.com/muesli/reflow/wordwrap" +) + +const instructionsURL = "https://raw.githubusercontent.com/launchdarkly/hello-%s/main/README.md" + +type showSDKInstructionsModel struct { + instructions string + sdk string + width int +} + +func NewShowSDKInstructionsModel() tea.Model { + return showSDKInstructionsModel{} +} + +func (m showSDKInstructionsModel) Init() tea.Cmd { + // send command to make request? + return nil +} + +func (m showSDKInstructionsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case fetchSDKInstructionsMsg: + url := fmt.Sprintf(instructionsURL, msg.canonicalName) + c := &http.Client{ + Timeout: 5 * time.Second, + } + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return m, sendErr(err) + } + resp, err := c.Do(req) + if err != nil { + return m, sendErr(err) + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return m, sendErr(err) + } + + if resp.StatusCode != 200 { + return m, sendErr(errors.NewError(fmt.Sprintf("could not find %s SDK instructions", msg.name))) + } + + m.sdk = msg.name + m.instructions = string(body) + } + + return m, nil +} + +func (m showSDKInstructionsModel) View() string { + style := lipgloss.NewStyle().Border(lipgloss.NormalBorder(), true, false) + md, err := m.renderMarkdown() + if err != nil { + return fmt.Sprintf("error rendering instructions: %s", err) + } + + return wordwrap.String( + fmt.Sprintf( + "Set up your application. Here are the steps to incorporate the LaunchDarkly %s SDK into your code.\n%s", + m.sdk, + style.Render(md), + ), + m.width, + ) +} + +func (m showSDKInstructionsModel) renderMarkdown() (string, error) { + out, err := glamour.Render(m.instructions, "auto") + if err != nil { + return "", err + } + + return out, nil +} From b9b146373a3858ecbb6d7234530f5e4eebf79616 Mon Sep 17 00:00:00 2001 From: Danny Olson Date: Fri, 29 Mar 2024 09:56:27 -0700 Subject: [PATCH 4/7] Can fetch SDK instructions --- internal/quickstart/choose_sdk.go | 51 ++++++++++++++++--------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/internal/quickstart/choose_sdk.go b/internal/quickstart/choose_sdk.go index 8f93f854..8bb97420 100644 --- a/internal/quickstart/choose_sdk.go +++ b/internal/quickstart/choose_sdk.go @@ -77,31 +77,32 @@ const clientSideSDK = "client" const serverSideSDK = "server" var SDKs = []sdkDetail{ - {DisplayName: "React", SDKType: clientSideSDK}, - {DisplayName: "Node.js (server-side)", SDKType: serverSideSDK}, - {DisplayName: "Python", SDKType: serverSideSDK}, - {DisplayName: "Java", SDKType: serverSideSDK}, - {DisplayName: ".NET (server-side)", SDKType: serverSideSDK}, - {DisplayName: "JavaScript", SDKType: clientSideSDK}, - {DisplayName: "Vue", SDKType: clientSideSDK}, - {DisplayName: "iOS", SDKType: clientSideSDK}, - {DisplayName: "Go", SDKType: serverSideSDK}, - {DisplayName: "Android", SDKType: clientSideSDK}, - {DisplayName: "React Native", SDKType: clientSideSDK}, - {DisplayName: "Ruby", SDKType: serverSideSDK}, - {DisplayName: "Flutter", SDKType: clientSideSDK}, - {DisplayName: ".NET (client-side)", SDKType: clientSideSDK}, - {DisplayName: "Erlang", SDKType: serverSideSDK}, - {DisplayName: "Rust", SDKType: serverSideSDK}, - {DisplayName: "Electron", SDKType: clientSideSDK}, - {DisplayName: "C/C++ (client-side)", SDKType: clientSideSDK}, - {DisplayName: "Roku", SDKType: clientSideSDK}, - {DisplayName: "Node.js (client-side)", SDKType: clientSideSDK}, - {DisplayName: "C/C++ (server-side)", SDKType: serverSideSDK}, - {DisplayName: "Lua", SDKType: serverSideSDK}, - {DisplayName: "Haskell", SDKType: serverSideSDK}, - {DisplayName: "Apex", SDKType: serverSideSDK}, - {DisplayName: "PHP", SDKType: serverSideSDK}, + // TODO: react is still internal + // {CanonicalName: "react", DisplayName: "React", SDKType: clientSideSDK}, + {CanonicalName: "node-server", DisplayName: "Node.js (server-side)", SDKType: serverSideSDK}, + {CanonicalName: "python", DisplayName: "Python", SDKType: serverSideSDK}, + {CanonicalName: "java", DisplayName: "Java", SDKType: serverSideSDK}, + {CanonicalName: "dotnet-server", DisplayName: ".NET (server-side)", SDKType: serverSideSDK}, + {CanonicalName: "js", DisplayName: "JavaScript", SDKType: clientSideSDK}, + {CanonicalName: "vue", DisplayName: "Vue", SDKType: clientSideSDK}, + {CanonicalName: "ios", DisplayName: "iOS", SDKType: clientSideSDK}, + {CanonicalName: "go", DisplayName: "Go", SDKType: serverSideSDK}, + {CanonicalName: "android", DisplayName: "Android", SDKType: clientSideSDK}, + {CanonicalName: "react-native", DisplayName: "React Native", SDKType: clientSideSDK}, + {CanonicalName: "ruby", DisplayName: "Ruby", SDKType: serverSideSDK}, + {CanonicalName: "flutter", DisplayName: "Flutter", SDKType: clientSideSDK}, + {CanonicalName: "dotnet-client", DisplayName: ".NET (client-side)", SDKType: clientSideSDK}, + {CanonicalName: "erlang", DisplayName: "Erlang", SDKType: serverSideSDK}, + {CanonicalName: "rust", DisplayName: "Rust", SDKType: serverSideSDK}, + {CanonicalName: "electron", DisplayName: "Electron", SDKType: clientSideSDK}, + {CanonicalName: "c-client", DisplayName: "C/C++ (client-side)", SDKType: clientSideSDK}, + {CanonicalName: "roku", DisplayName: "Roku", SDKType: clientSideSDK}, + {CanonicalName: "node-client", DisplayName: "Node.js (client-side)", SDKType: clientSideSDK}, + {CanonicalName: "c-server", DisplayName: "C/C++ (server-side)", SDKType: serverSideSDK}, + {CanonicalName: "lua", DisplayName: "Lua", SDKType: serverSideSDK}, + {CanonicalName: "haskell", DisplayName: "Haskell", SDKType: serverSideSDK}, + {CanonicalName: "apex", DisplayName: "Apex", SDKType: serverSideSDK}, + {CanonicalName: "php", DisplayName: "PHP", SDKType: serverSideSDK}, } func sdksToItems() []list.Item { From b5caf745e934d66af543f659f0336dd92d6730ff Mon Sep 17 00:00:00 2001 From: Danny Olson Date: Fri, 29 Mar 2024 10:04:09 -0700 Subject: [PATCH 5/7] Minor cleanup --- internal/quickstart/choose_sdk.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/internal/quickstart/choose_sdk.go b/internal/quickstart/choose_sdk.go index 8bb97420..9d6379bc 100644 --- a/internal/quickstart/choose_sdk.go +++ b/internal/quickstart/choose_sdk.go @@ -16,9 +16,14 @@ var ( selectedSdkItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) ) +const ( + clientSideSDK = "client" + serverSideSDK = "server" +) + type chooseSDKModel struct { - selectedSDK sdkDetail list list.Model + selectedSDK sdkDetail } func NewChooseSDKModel() tea.Model { @@ -66,16 +71,13 @@ func (m chooseSDKModel) View() string { } type sdkDetail struct { - CanonicalName string `json:"canonicalName"` - DisplayName string `json:"displayName"` - SDKType string `json:"sdkType"` + CanonicalName string + DisplayName string + SDKType string } func (s sdkDetail) FilterValue() string { return "" } -const clientSideSDK = "client" -const serverSideSDK = "server" - var SDKs = []sdkDetail{ // TODO: react is still internal // {CanonicalName: "react", DisplayName: "React", SDKType: clientSideSDK}, From 31460a13480e053ac3af4bb976a881272e6e2f7c Mon Sep 17 00:00:00 2001 From: Danny Olson Date: Fri, 29 Mar 2024 10:10:47 -0700 Subject: [PATCH 6/7] Small changes --- internal/quickstart/container.go | 6 ++---- internal/quickstart/show_sdk_instructions.go | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/internal/quickstart/container.go b/internal/quickstart/container.go index c5b848d1..5e0c01d5 100644 --- a/internal/quickstart/container.go +++ b/internal/quickstart/container.go @@ -77,10 +77,8 @@ func (m ContainerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.currentStep += 1 } case showSDKInstructionsStep: - updated, cmd := m.steps[showSDKInstructionsStep].Update(msg) - if _, ok := updated.(showSDKInstructionsModel); ok { - m.currentStep += 1 - } + _, cmd := m.steps[showSDKInstructionsStep].Update(msg) + m.currentStep += 1 return m, cmd default: diff --git a/internal/quickstart/show_sdk_instructions.go b/internal/quickstart/show_sdk_instructions.go index 3593d315..e498b963 100644 --- a/internal/quickstart/show_sdk_instructions.go +++ b/internal/quickstart/show_sdk_instructions.go @@ -18,7 +18,6 @@ const instructionsURL = "https://raw.githubusercontent.com/launchdarkly/hello-%s type showSDKInstructionsModel struct { instructions string sdk string - width int } func NewShowSDKInstructionsModel() tea.Model { @@ -75,7 +74,7 @@ func (m showSDKInstructionsModel) View() string { m.sdk, style.Render(md), ), - m.width, + 0, ) } From b76382b977dffdfdeb20a3950b9659b1e60717cc Mon Sep 17 00:00:00 2001 From: Danny Olson Date: Mon, 1 Apr 2024 10:33:42 -0700 Subject: [PATCH 7/7] feat: sdk instruction find and replace (#94) replace flag key placeholder in SDK instructions --- internal/quickstart/choose_sdk.go | 58 ++++++++++---------- internal/quickstart/container.go | 13 +++-- internal/quickstart/messages.go | 12 ++++ internal/quickstart/show_sdk_instructions.go | 10 ++-- internal/sdks/sdks.go | 24 ++++++++ internal/sdks/sdks_test.go | 45 +++++++++++++++ 6 files changed, 124 insertions(+), 38 deletions(-) create mode 100644 internal/sdks/sdks.go create mode 100644 internal/sdks/sdks_test.go diff --git a/internal/quickstart/choose_sdk.go b/internal/quickstart/choose_sdk.go index 9d6379bc..8ba59130 100644 --- a/internal/quickstart/choose_sdk.go +++ b/internal/quickstart/choose_sdk.go @@ -71,40 +71,38 @@ func (m chooseSDKModel) View() string { } type sdkDetail struct { - CanonicalName string - DisplayName string - SDKType string + canonicalName string + displayName string + kind string } func (s sdkDetail) FilterValue() string { return "" } var SDKs = []sdkDetail{ - // TODO: react is still internal - // {CanonicalName: "react", DisplayName: "React", SDKType: clientSideSDK}, - {CanonicalName: "node-server", DisplayName: "Node.js (server-side)", SDKType: serverSideSDK}, - {CanonicalName: "python", DisplayName: "Python", SDKType: serverSideSDK}, - {CanonicalName: "java", DisplayName: "Java", SDKType: serverSideSDK}, - {CanonicalName: "dotnet-server", DisplayName: ".NET (server-side)", SDKType: serverSideSDK}, - {CanonicalName: "js", DisplayName: "JavaScript", SDKType: clientSideSDK}, - {CanonicalName: "vue", DisplayName: "Vue", SDKType: clientSideSDK}, - {CanonicalName: "ios", DisplayName: "iOS", SDKType: clientSideSDK}, - {CanonicalName: "go", DisplayName: "Go", SDKType: serverSideSDK}, - {CanonicalName: "android", DisplayName: "Android", SDKType: clientSideSDK}, - {CanonicalName: "react-native", DisplayName: "React Native", SDKType: clientSideSDK}, - {CanonicalName: "ruby", DisplayName: "Ruby", SDKType: serverSideSDK}, - {CanonicalName: "flutter", DisplayName: "Flutter", SDKType: clientSideSDK}, - {CanonicalName: "dotnet-client", DisplayName: ".NET (client-side)", SDKType: clientSideSDK}, - {CanonicalName: "erlang", DisplayName: "Erlang", SDKType: serverSideSDK}, - {CanonicalName: "rust", DisplayName: "Rust", SDKType: serverSideSDK}, - {CanonicalName: "electron", DisplayName: "Electron", SDKType: clientSideSDK}, - {CanonicalName: "c-client", DisplayName: "C/C++ (client-side)", SDKType: clientSideSDK}, - {CanonicalName: "roku", DisplayName: "Roku", SDKType: clientSideSDK}, - {CanonicalName: "node-client", DisplayName: "Node.js (client-side)", SDKType: clientSideSDK}, - {CanonicalName: "c-server", DisplayName: "C/C++ (server-side)", SDKType: serverSideSDK}, - {CanonicalName: "lua", DisplayName: "Lua", SDKType: serverSideSDK}, - {CanonicalName: "haskell", DisplayName: "Haskell", SDKType: serverSideSDK}, - {CanonicalName: "apex", DisplayName: "Apex", SDKType: serverSideSDK}, - {CanonicalName: "php", DisplayName: "PHP", SDKType: serverSideSDK}, + {canonicalName: "react", displayName: "React", kind: clientSideSDK}, + {canonicalName: "node-server", displayName: "Node.js (server-side)", kind: serverSideSDK}, + {canonicalName: "python", displayName: "Python", kind: serverSideSDK}, + {canonicalName: "java", displayName: "Java", kind: serverSideSDK}, + {canonicalName: "dotnet-server", displayName: ".NET (server-side)", kind: serverSideSDK}, + {canonicalName: "js", displayName: "JavaScript", kind: clientSideSDK}, + {canonicalName: "ios-swift", displayName: "iOS", kind: clientSideSDK}, + {canonicalName: "go", displayName: "Go", kind: serverSideSDK}, + {canonicalName: "android", displayName: "Android", kind: clientSideSDK}, + {canonicalName: "react-native", displayName: "React Native", kind: clientSideSDK}, + {canonicalName: "ruby", displayName: "Ruby", kind: serverSideSDK}, + {canonicalName: "flutter", displayName: "Flutter", kind: clientSideSDK}, + {canonicalName: "dotnet-client", displayName: ".NET (client-side)", kind: clientSideSDK}, + {canonicalName: "erlang", displayName: "Erlang", kind: serverSideSDK}, + {canonicalName: "rust", displayName: "Rust", kind: serverSideSDK}, + {canonicalName: "electron", displayName: "Electron", kind: clientSideSDK}, + {canonicalName: "c-client", displayName: "C/C++ (client-side)", kind: clientSideSDK}, + {canonicalName: "roku", displayName: "Roku", kind: clientSideSDK}, + {canonicalName: "node-client", displayName: "Node.js (client-side)", kind: clientSideSDK}, + {canonicalName: "c-server", displayName: "C/C++ (server-side)", kind: serverSideSDK}, + {canonicalName: "lua-server", displayName: "Lua", kind: serverSideSDK}, + {canonicalName: "haskell-server", displayName: "Haskell", kind: serverSideSDK}, + {canonicalName: "apex-server", displayName: "Apex", kind: serverSideSDK}, + {canonicalName: "php", displayName: "PHP", kind: serverSideSDK}, } func sdksToItems() []list.Item { @@ -127,7 +125,7 @@ func (d sdkDelegate) Render(w io.Writer, m list.Model, index int, listItem list. return } - str := fmt.Sprintf("%d. %s", index+1, i.DisplayName) + str := fmt.Sprintf("%d. %s", index+1, i.displayName) fn := sdkStyle.Render if index == m.Index() { diff --git a/internal/quickstart/container.go b/internal/quickstart/container.go index f9378c85..510c00f5 100644 --- a/internal/quickstart/container.go +++ b/internal/quickstart/container.go @@ -103,17 +103,22 @@ func (m ContainerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch m.currentStep { case showSDKInstructionsStep: updated, cmd = m.steps[showSDKInstructionsStep].Update(fetchSDKInstructionsMsg{ - canonicalName: m.sdk.CanonicalName, - name: m.sdk.DisplayName, + canonicalName: m.sdk.canonicalName, + flagKey: m.flagKey, + name: m.sdk.displayName, }) if model, ok := updated.(showSDKInstructionsModel); ok { - model.sdk = m.sdk.DisplayName + model.sdk = m.sdk.displayName m.steps[showSDKInstructionsStep] = model } default: } case errMsg: m.err = msg.err + case noInstructionsMsg: + m.currentStep += 1 + + return m, cmd default: } @@ -123,7 +128,7 @@ func (m ContainerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m ContainerModel) View() string { // TODO: remove after creating more steps if m.currentStep > showSDKInstructionsStep { - return fmt.Sprintf("created flag %s\nselected the %s SDK", m.flagKey, m.sdk.DisplayName) + return fmt.Sprintf("created flag %s\nselected the %s SDK", m.flagKey, m.sdk.displayName) } out := fmt.Sprintf("\nStep %d of %d\n"+m.steps[m.currentStep].View(), m.currentStep+1, len(m.steps)) diff --git a/internal/quickstart/messages.go b/internal/quickstart/messages.go index b25dccc6..c6c5adbb 100644 --- a/internal/quickstart/messages.go +++ b/internal/quickstart/messages.go @@ -4,9 +4,12 @@ import tea "github.com/charmbracelet/bubbletea" type fetchSDKInstructionsMsg struct { canonicalName string + flagKey string name string } +// errMsg is sent when there is an error in one of the steps that the container model needs to +// know about. type errMsg struct { err error } @@ -16,3 +19,12 @@ func sendErr(err error) tea.Cmd { return errMsg{err: err} } } + +// noInstructionsMsg is sent when we can't find the SDK instructions repository for the given SDK. +type noInstructionsMsg struct{} + +func sendNoInstructions() tea.Cmd { + return func() tea.Msg { + return noInstructionsMsg{} + } +} diff --git a/internal/quickstart/show_sdk_instructions.go b/internal/quickstart/show_sdk_instructions.go index e498b963..d4067d63 100644 --- a/internal/quickstart/show_sdk_instructions.go +++ b/internal/quickstart/show_sdk_instructions.go @@ -3,7 +3,7 @@ package quickstart import ( "fmt" "io" - "ldcli/internal/errors" + "ldcli/internal/sdks" "net/http" "time" @@ -50,12 +50,14 @@ func (m showSDKInstructionsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, sendErr(err) } - if resp.StatusCode != 200 { - return m, sendErr(errors.NewError(fmt.Sprintf("could not find %s SDK instructions", msg.name))) + if resp.StatusCode == 404 { + m.sdk = msg.name + + return m, sendNoInstructions() } m.sdk = msg.name - m.instructions = string(body) + m.instructions = sdks.ReplaceFlagKey(string(body), msg.flagKey) } return m, nil diff --git a/internal/sdks/sdks.go b/internal/sdks/sdks.go new file mode 100644 index 00000000..18c3c023 --- /dev/null +++ b/internal/sdks/sdks.go @@ -0,0 +1,24 @@ +package sdks + +import ( + "strings" +) + +// ReplaceFlagKey changes the placeholder flag key in the SDK instructions to the flag key from +// the user. +func ReplaceFlagKey(instructions string, key string) string { + r := strings.NewReplacer( + "my-flag-key", + key, + "my-flag", + key, + "my-boolean-flag", + key, + "FLAG_KEY", + key, + "", + key, + ) + + return r.Replace(instructions) +} diff --git a/internal/sdks/sdks_test.go b/internal/sdks/sdks_test.go new file mode 100644 index 00000000..e8d5ed8b --- /dev/null +++ b/internal/sdks/sdks_test.go @@ -0,0 +1,45 @@ +package sdks_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "ldcli/internal/sdks" +) + +func TestReplaceFlagKey(t *testing.T) { + tests := map[string]struct { + body string + expected string + }{ + "replaces placeholder my-flag-key": { + body: "# title ```const featureFlagKey = \"my-flag-key\"```", + expected: "# title ```const featureFlagKey = \"real-flag-key\"```", + }, + "replaces placeholder my-flag": { + body: "# title ```const featureFlagKey = \"my-flag\"```", + expected: "# title ```const featureFlagKey = \"real-flag-key\"```", + }, + "replaces placeholder my-boolean-flag": { + body: "# title ```const featureFlagKey = \"my-boolean-flag\"```", + expected: "# title ```const featureFlagKey = \"real-flag-key\"```", + }, + "replaces placeholder FLAG_KEY": { + body: "# title ```const featureFlagKey = \"my-boolean-flag\"```", + expected: "# title ```const featureFlagKey = \"real-flag-key\"```", + }, + "replaces placeholder ": { + body: "# title ```hello_erlang_server:get(<<\"FLAG_KEY\">>)```", + expected: "# title ```hello_erlang_server:get(<<\"real-flag-key\">>)```", + }, + } + for name, tt := range tests { + tt := tt + t.Run(name, func(t *testing.T) { + updated := sdks.ReplaceFlagKey(tt.body, "real-flag-key") + + assert.Equal(t, string(tt.expected), string(updated)) + }) + } +}