Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: quickstart --from flag #1291

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions cmd/quickstart.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type QuickstartFlags struct {
Schema string `json:"schema"`
OutDir string `json:"out-dir"`
TargetType string `json:"target"`
From string `json:"from"`
}

//go:embed sample_openapi.yaml
Expand Down Expand Up @@ -75,6 +76,11 @@ var quickstartCmd = &model.ExecutableCommand[QuickstartFlags]{
Shorthand: "t",
Description: fmt.Sprintf("language to generate sdk for (available options: [%s])", strings.Join(prompts.GetSupportedTargets(), ", ")),
},
flag.StringFlag{
Name: "from",
Shorthand: "f",
Description: "template to use for the quickstart command.\nCreate a new sandbox at https://app.speakeasy.com/sandbox",
},
},
}

Expand All @@ -97,8 +103,6 @@ func quickstartExec(ctx context.Context, flags QuickstartFlags) error {
return ErrWorkflowExists
}

log.From(ctx).PrintfStyled(styles.DimmedItalic, "\nYour first SDK is a few short questions away...\n")

quickstartObj := prompts.Quickstart{
WorkflowFile: &workflow.Workflow{
Version: workflow.WorkflowVersion,
Expand All @@ -116,6 +120,11 @@ func quickstartExec(ctx context.Context, flags QuickstartFlags) error {
quickstartObj.Defaults.TargetType = &flags.TargetType
}

if flags.From != "" {
quickstartObj.Defaults.Blueprint = &flags.From
quickstartObj.IsUsingBlueprint = true
}

nextState := prompts.SourceBase
for nextState != prompts.Complete {
stateFunc := prompts.StateMapping[nextState]
Expand Down Expand Up @@ -258,6 +267,20 @@ func quickstartExec(ctx context.Context, flags QuickstartFlags) error {
quickstartObj.WorkflowFile.Sources[sourceName].Inputs[0].Location = workflow.LocationString(referencePath)
}

// If we are using a blueprint template, the original location will be a
// tempfile. We want therefore to move the tempfile to the output directory,
// and update the workflow file to point to the new location.
if quickstartObj.IsUsingBlueprint {
oldInput := quickstartObj.WorkflowFile.Sources[sourceName].Inputs[0].Location

newPath := filepath.Join(outDir, "openapi.yaml")
if err := os.Rename(oldInput.Resolve(), newPath); err != nil {
return errors.Wrapf(err, "failed to rename blueprint to openapi.yaml")
}

quickstartObj.WorkflowFile.Sources[sourceName].Inputs[0].Location = workflow.LocationString(newPath)
}

// Make sure the workflow file stays up to date
run.Migrate(ctx, quickstartObj.WorkflowFile)

Expand Down
104 changes: 101 additions & 3 deletions prompts/sources.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package prompts

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
Expand Down Expand Up @@ -180,8 +182,10 @@ func sourceBaseForm(ctx context.Context, quickstart *Quickstart) (*QuickstartSta
defer cancel()
recentGenerations, err := remote.GetRecentWorkspaceGenerations(timeout)

// Retrieve recent namespaces and check if there are any available.
hasRecentGenerations := err == nil && len(recentGenerations) > 0
hasBlueprint := quickstart.Defaults.Blueprint != nil && *quickstart.Defaults.Blueprint != ""

// Retrieve recent namespaces and check if there are any available. If --blueprint is provided, we will not check for recent generations.
hasRecentGenerations := !hasBlueprint && err == nil && len(recentGenerations) > 0

// Determine if we should use a remote source. Defaults to true before the user
// has interacted with the form.
Expand Down Expand Up @@ -213,7 +217,29 @@ func sourceBaseForm(ctx context.Context, quickstart *Quickstart) (*QuickstartSta
}
}

if quickstart.Defaults.SchemaPath != nil {
if hasBlueprint {
blueprintFile, err := fetchAndSaveBlueprint(ctx, *quickstart.Defaults.Blueprint)
if err == nil {
fileLocation = blueprintFile

fmt.Println(
styles.RenderInfoMessage(
fmt.Sprintf("Using sandbox session '%s'", *quickstart.Defaults.Blueprint),
) + "\n",
)
} else {
// fallthrough
fmt.Println(
styles.RenderInfoMessage(
fmt.Sprintf("Could not find sandbox session '%s'. Continuing with quickstart...", *quickstart.Defaults.Blueprint),
) + "\n",
)
}
}

if hasBlueprint && fileLocation != "" {
// noop
} else if quickstart.Defaults.SchemaPath != nil {
fileLocation = *quickstart.Defaults.SchemaPath
} else if useRemoteSource && selectedRegistryUri != "" {
// The workflow file will be updated with a registry based input like:
Expand Down Expand Up @@ -657,3 +683,75 @@ func configureRegistry(source *workflow.Source, orgSlug, workspaceSlug, sourceNa
source.Registry = registryEntry
return nil
}

type FormatType string

const (
FormatJSON FormatType = "json"
FormatYAML FormatType = "yaml"
)

type blueprintRequest struct {
ID string `json:"id"`
}

type blueprintResponse struct {
ID string `json:"id"`
Spec string `json:"spec"`
CreatedAt time.Time `json:"created_at"`
Format FormatType `json:"format"`
}

var (
ErrMsgFailedToFetchBlueprint = errors.New("failed to fetch sandbox session")
ErrMsgFailedToSaveBlueprint = errors.New("failed to save sandbox session")
ErrMsgFailedToDecodeBlueprint = errors.New("failed to decode sandbox session")
)

func fetchAndSaveBlueprint(ctx context.Context, blueprintID string) (string, error) {
baseURL := "https://api.speakeasy.com"
url := fmt.Sprintf("%s/v1/schema_store", baseURL)

var reqBody blueprintRequest
reqBody.ID = blueprintID
body, err := json.Marshal(reqBody)
if err != nil {
return "", ErrMsgFailedToDecodeBlueprint
}
req, err := http.NewRequest(http.MethodGet, url, bytes.NewBuffer(body))
if err != nil {
return "", ErrMsgFailedToFetchBlueprint
}
req.Header.Set("Content-Type", "application/json")

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return "", ErrMsgFailedToFetchBlueprint
}

var respBody blueprintResponse
err = json.NewDecoder(resp.Body).Decode(&respBody)
if err != nil {
return "", err
}

tempDir := os.TempDir()
tempFile, err := os.Create(filepath.Join(tempDir, fmt.Sprintf("sandbox-%s.%s", respBody.ID, respBody.Format)))
if err != nil {
return "", ErrMsgFailedToSaveBlueprint
}
defer tempFile.Close()

_, err = tempFile.WriteString(respBody.Spec)
if err != nil {
return "", ErrMsgFailedToSaveBlueprint
}

return tempFile.Name(), nil
}
2 changes: 2 additions & 0 deletions prompts/statemappings.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ type Quickstart struct {
LanguageConfigs map[string]*config.Configuration
Defaults Defaults
IsUsingSampleOpenAPISpec bool
IsUsingBlueprint bool
SDKName string
}

type Defaults struct {
SchemaPath *string
TargetType *string
Blueprint *string
}

// Define constants using iota
Expand Down
Loading