Skip to content

Commit

Permalink
feat: add sdk instructions step to quickstart (#91)
Browse files Browse the repository at this point in the history
Create choose SDK setup step
  • Loading branch information
dbolson authored Apr 1, 2024
1 parent 01194df commit bf4aba6
Show file tree
Hide file tree
Showing 7 changed files with 270 additions and 46 deletions.
74 changes: 39 additions & 35 deletions internal/quickstart/choose_sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,19 @@ var (
selectedSdkItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170"))
)

const (
clientSideSDK = "client"
serverSideSDK = "server"
)

type chooseSDKModel struct {
list list.Model
selectedSdk sdkDetail
selectedSDK sdkDetail
}

func NewChooseSDKModel() tea.Model {
l := list.New(sdksToItems(), sdkDelegate{}, 30, 14)
l.Title = "Select your SDK:\n"
l.Title = "Select your SDK:\n\n" // extra newlines to show pagination
// reset title styles
l.Styles.Title = lipgloss.NewStyle()
l.Styles.TitleBar = lipgloss.NewStyle()
Expand All @@ -37,7 +42,9 @@ func NewChooseSDKModel() tea.Model {
}
}

func (m chooseSDKModel) Init() tea.Cmd { return nil }
func (m chooseSDKModel) Init() tea.Cmd {
return nil
}

func (m chooseSDKModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
Expand All @@ -47,7 +54,7 @@ func (m chooseSDKModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case key.Matches(msg, keys.Enter):
i, ok := m.list.SelectedItem().(sdkDetail)
if ok {
m.selectedSdk = i
m.selectedSDK = i
}
case key.Matches(msg, keys.Quit):
return m, tea.Quit
Expand All @@ -64,41 +71,38 @@ func (m chooseSDKModel) View() string {
}

type sdkDetail struct {
DisplayName string `json:"displayName"`
SDKType string `json:"sdkType"`
canonicalName string
displayName string
kind string
}

func (s sdkDetail) FilterValue() string { return "" }

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},
{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 {
Expand All @@ -121,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() {
Expand Down
51 changes: 41 additions & 10 deletions internal/quickstart/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -38,6 +39,7 @@ func NewContainerModel(flagsClient flags.Client) tea.Model {
steps: []tea.Model{
NewCreateFlagModel(flagsClient),
NewChooseSDKModel(),
NewShowSDKInstructionsModel(),
},
}
}
Expand All @@ -47,9 +49,17 @@ 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:
Expand All @@ -73,31 +83,52 @@ func (m ContainerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case chooseSDKStep:
updated, _ := m.steps[chooseSDKStep].Update(msg)
if model, ok := updated.(chooseSDKModel); ok {
// no error state for this step
m.sdk = model.selectedSdk
m.sdk = model.selectedSDK
m.currentStep += 1
}
case showSDKInstructionsStep:
_, cmd := m.steps[showSDKInstructionsStep].Update(msg)
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, _ := m.steps[m.currentStep].Update(msg)
updated, cmd := m.steps[m.currentStep].Update(msg)
m.steps[m.currentStep] = updated

return m, cmd
}
switch m.currentStep {
case showSDKInstructionsStep:
updated, cmd = m.steps[showSDKInstructionsStep].Update(fetchSDKInstructionsMsg{
canonicalName: m.sdk.canonicalName,
flagKey: m.flagKey,
name: m.sdk.displayName,
})
if model, ok := updated.(showSDKInstructionsModel); ok {
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:
}

return m, nil
return m, cmd
}

func (m ContainerModel) View() string {
// TODO: remove after creating more steps
if m.currentStep > chooseSDKStep {
return fmt.Sprintf("created flag %s\nselected the %s SDK", m.flagKey, m.sdk.DisplayName)
if m.currentStep > showSDKInstructionsStep {
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))
Expand Down
2 changes: 1 addition & 1 deletion internal/quickstart/create_flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func NewCreateFlagModel(client flags.Client) tea.Model {
}
}

func (p createFlagModel) Init() tea.Cmd {
func (m createFlagModel) Init() tea.Cmd {
return nil
}

Expand Down
30 changes: 30 additions & 0 deletions internal/quickstart/messages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package quickstart

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
}

func sendErr(err error) tea.Cmd {
return func() tea.Msg {
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{}
}
}
90 changes: 90 additions & 0 deletions internal/quickstart/show_sdk_instructions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package quickstart

import (
"fmt"
"io"
"ldcli/internal/sdks"
"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
}

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 == 404 {
m.sdk = msg.name

return m, sendNoInstructions()
}

m.sdk = msg.name
m.instructions = sdks.ReplaceFlagKey(string(body), msg.flagKey)
}

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),
),
0,
)
}

func (m showSDKInstructionsModel) renderMarkdown() (string, error) {
out, err := glamour.Render(m.instructions, "auto")
if err != nil {
return "", err
}

return out, nil
}
24 changes: 24 additions & 0 deletions internal/sdks/sdks.go
Original file line number Diff line number Diff line change
@@ -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,
"<flag key>",
key,
)

return r.Replace(instructions)
}
Loading

0 comments on commit bf4aba6

Please sign in to comment.