From 4e16f138dd6b747893cc79a96039fafa3acff848 Mon Sep 17 00:00:00 2001 From: Jeff Andersen Date: Thu, 26 Oct 2017 14:35:20 -0300 Subject: [PATCH] Normalize name/title logic and translation --- CHANGELOG.md | 1 + clients/teams.go | 5 ++- cmd/create.go | 17 +++----- cmd/init.go | 2 +- cmd/main.go | 64 ++++++++++++++++++++++++++++ cmd/projects.go | 61 +++++++++++---------------- cmd/teams.go | 38 +++++++---------- cmd/update.go | 20 ++++----- prompts/prompts.go | 102 ++++++++++++--------------------------------- prompts/selects.go | 8 ++-- 10 files changed, 155 insertions(+), 163 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5c23a9..8612c00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Project create failure due to Name/Title change - Set-role should error before prompts - Context would panic due to missing session +- Change logic for inferring object title from name ## [0.8.1] - 2017-10-24 diff --git a/clients/teams.go b/clients/teams.go index 442d172..f0a9f47 100644 --- a/clients/teams.go +++ b/clients/teams.go @@ -12,6 +12,7 @@ import ( // TeamMembersCount groups a team name with the amount of members the team has type TeamMembersCount struct { Name string + Title string Members int } @@ -62,7 +63,8 @@ func FetchTeamsMembersCount(ctx context.Context, c *iClient.Identity) ([]TeamMem for _, t := range teams { id := t.ID.String() - name := string(t.Body.Name) + name := string(t.Body.Label) + title := string(t.Body.Name) go func() { members, err := FetchTeamMembers(ctx, id, c) @@ -74,6 +76,7 @@ func FetchTeamsMembersCount(ctx context.Context, c *iClient.Identity) ([]TeamMem res <- TeamMembersCount{ Name: name, + Title: title, Members: len(members), } }() diff --git a/cmd/create.go b/cmd/create.go index 5d4d4c1..e4e0a7c 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -40,6 +40,7 @@ func init() { projectFlag(), planFlag(), regionFlag(), + titleFlag(), cli.StringFlag{ Name: "product", Usage: "Create a resource for this product", @@ -62,7 +63,7 @@ func create(cliCtx *cli.Context) error { return err } - resourceTitle, err := optionalArgTitle(cliCtx, 0, "resource") + resourceName, resourceTitle, err := promptNameAndTitle(cliCtx, "resource", true, true) if err != nil { return err } @@ -156,7 +157,7 @@ func create(cliCtx *cli.Context) error { var project *mModels.Project if len(projects) > 0 { - pidx, _, err := prompts.SelectProject(projects, projectName, true) + pidx, _, err := prompts.SelectProject(projects, projectName, true, true) if err != nil { return prompts.HandleSelectError(err, "Could not select project.") } @@ -166,11 +167,6 @@ func create(cliCtx *cli.Context) error { } } - resourceTitle, err = prompts.ResourceTitle(resourceTitle, false) - if err != nil { - return cli.NewExitError("Could not name the resource: "+err.Error(), -1) - } - descriptor := "a custom resource" if !custom { descriptor = "an instance of " + string(product.Body.Name) @@ -182,7 +178,7 @@ func create(cliCtx *cli.Context) error { } op, err := createResource(ctx, cfg, teamID, s, client.Provisioning, custom, product, plan, region, - project, resourceTitle, dontWait) + project, resourceName, resourceTitle, dontWait) if err != nil { return cli.NewExitError("Could not create resource: "+err.Error(), -1) } @@ -206,7 +202,7 @@ func create(cliCtx *cli.Context) error { func createResource(ctx context.Context, cfg *config.Config, teamID *manifold.ID, s session.Session, pClient *provisioning.Provisioning, custom bool, product *cModels.Product, plan *cModels.Plan, - region *cModels.Region, project *mModels.Project, resourceTitle string, dontWait bool) (*pModels.Operation, error) { + region *cModels.Region, project *mModels.Project, resourceName, resourceTitle string, dontWait bool) (*pModels.Operation, error) { a, err := analytics.New(cfg, s) if err != nil { @@ -243,7 +239,6 @@ func createResource(ctx context.Context, cfg *config.Config, teamID *manifold.ID typeStr := "operation" version := int64(1) state := "provision" - empty := "" curTime := strfmt.DateTime(time.Now()) op := &pModels.Operation{ ID: ID, @@ -251,7 +246,7 @@ func createResource(ctx context.Context, cfg *config.Config, teamID *manifold.ID Version: &version, Body: &pModels.Provision{ ResourceID: resourceID, - Label: &empty, + Label: &resourceName, Name: &resourceTitle, Source: &source, PlanID: planID, diff --git a/cmd/init.go b/cmd/init.go index 11a38d5..8522ad6 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -66,7 +66,7 @@ func initDir(cliCtx *cli.Context) error { return errs.ErrNoProjects } - pIdx, _, err := prompts.SelectProject(ps, projectName, true) + pIdx, _, err := prompts.SelectProject(ps, projectName, true, true) if err != nil { return prompts.HandleSelectError(err, "Could not select project.") } diff --git a/cmd/main.go b/cmd/main.go index a5528eb..972523c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,11 +3,14 @@ package main import ( "fmt" "os" + "strings" "github.com/urfave/cli" + "github.com/manifoldco/go-manifold" "github.com/manifoldco/manifold-cli/config" "github.com/manifoldco/manifold-cli/plugins" + "github.com/manifoldco/manifold-cli/prompts" ) var cmds []cli.Command @@ -67,3 +70,64 @@ var helpCommand = cli.Command{ return nil }, } + +// generateName makes a title lowercase and replace spaces with dashes +func generateName(title string) manifold.Label { + name := strings.Replace(strings.ToLower(title), " ", "-", -1) + return manifold.Label(name) +} + +// generateTitle makes a name capitalized and replace dashes with spaaces +func generateTitle(name string) manifold.Name { + title := strings.Title(strings.Replace(name, "-", " ", -1)) + return manifold.Name(title) +} + +// promptNameAndTitle encapsulates the logic for accepting a name as the first +// positional argument (optionally), and a title flag (optionally) +// returning generated values accepted by the user +func promptNameAndTitle(ctx *cli.Context, objectName string, shouldInferTitle, allowEmpty bool) (string, string, error) { + // The user may supply a name value as the first positional arg + argName, err := optionalArgName(ctx, 0, "name") + if err != nil { + return "", "", err + } + + // The user may supply a title value from a flag, validate it + flagTitle := ctx.String("title") + + // Create the title based on the name argument + return createNameAndTitle(ctx, objectName, argName, flagTitle, shouldInferTitle, true, allowEmpty) +} + +func createNameAndTitle(ctx *cli.Context, objectName, argName, flagTitle string, shouldInferTitle, shouldAccept, allowEmpty bool) (string, string, error) { + var name, title string + + // If no name value is supplied, prompt for it + // otherwise validate the given value + shouldAcceptName := shouldAccept && argName != "" + nameValue, err := prompts.Name(objectName, argName, shouldAcceptName, allowEmpty) + if err != nil { + return name, title, err + } + name = nameValue + + // We will automatically validate/accept a title given as flag + shouldAcceptTitle := shouldAccept && flagTitle != "" + // If we shouldn't infer the title, do not automatically accept a title value + if !shouldInferTitle { + shouldAcceptTitle = false + } + defaultTitle := flagTitle + if flagTitle == "" && shouldInferTitle { + // If no flag is present, we will infer the title from the validated name + defaultTitle = string(generateTitle(name)) + } + titleValue, err := prompts.Title(objectName, defaultTitle, shouldAcceptTitle, allowEmpty) + if err != nil { + return name, title, err + } + title = titleValue + + return name, title, nil +} diff --git a/cmd/projects.go b/cmd/projects.go index fe8119f..4b50d7a 100644 --- a/cmd/projects.go +++ b/cmd/projects.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "strings" "time" @@ -38,8 +37,8 @@ func init() { { Name: "create", Usage: "Create a new project", - Flags: teamFlags, - ArgsUsage: "[project-title]", + Flags: append(teamFlags, titleFlag()), + ArgsUsage: "[project-name]", Action: middleware.Chain(middleware.EnsureSession, middleware.LoadTeamPrefs, createProjectCmd), }, @@ -117,21 +116,15 @@ func createProjectCmd(cliCtx *cli.Context) error { return errUserActionAsTeam } - projectTitle, err := optionalArgTitle(cliCtx, 0, "title") + projectName, projectTitle, err := promptNameAndTitle(cliCtx, "project", true, false) if err != nil { return err } - autoSelect := projectTitle != "" - projectTitle, err = prompts.ProjectTitle(projectTitle, autoSelect) - if err != nil { - return prompts.HandleSelectError(err, "Failed to select project title") - } - params := projectClient.NewPostProjectsParamsWithContext(ctx) body := &mModels.CreateProjectBody{ Name: manifold.Name(projectTitle), - Label: generateName(projectTitle), + Label: manifold.Label(projectName), } if teamID == nil { @@ -189,7 +182,7 @@ func listProjectsCmd(cliCtx *cli.Context) error { fmt.Fprintf(w, "%s\n\n", color.Bold("Project")) for _, project := range projects { - fmt.Fprintf(w, "%s\n", project.Body.Label) + fmt.Fprintf(w, "%s (%s)\n", project.Body.Label, color.Faint(project.Body.Name)) } return w.Flush() } @@ -201,40 +194,40 @@ func updateProjectCmd(cliCtx *cli.Context) error { return err } - teamID, err := validateTeamID(cliCtx) - if err != nil { - return err - } - projectName, err := optionalArgName(cliCtx, 0, "project") if err != nil { return err } - newProjectTitle, err := validateTitle(cliCtx, "title", "project") + teamID, err := validateTeamID(cliCtx) if err != nil { return err } - projectDescription := cliCtx.String("description") - client, err := api.New(api.Marketplace) if err != nil { return err } - p, err := selectProject(ctx, projectName, teamID, client.Marketplace) + p, err := selectProject(ctx, projectName, teamID, client.Marketplace, false) if err != nil { return err } - autoSelectTitle := newProjectTitle != "" - newProjectTitle, err = prompts.ProjectTitle(newProjectTitle, autoSelectTitle) + providedTitle := cliCtx.String("title") + if providedTitle == "" { + providedTitle = string(p.Body.Name) + } + newName, newTitle, err := createNameAndTitle(cliCtx, "project", string(p.Body.Label), providedTitle, true, false, false) if err != nil { - return prompts.HandleSelectError(err, "Could not select project") + return err } + projectDescription := cliCtx.String("description") autoSelectDescription := projectDescription != "" + if projectDescription == "" { + projectDescription = p.Body.Description + } projectDescription, err = prompts.ProjectDescription(projectDescription, autoSelectDescription) if err != nil { return prompts.HandleSelectError(err, "Could not add description to project") @@ -242,8 +235,8 @@ func updateProjectCmd(cliCtx *cli.Context) error { params := projectClient.NewPatchProjectsIDParamsWithContext(ctx) body := &mModels.PublicUpdateProjectBody{ - Name: manifold.Name(newProjectTitle), - Label: generateName(newProjectTitle), + Name: manifold.Name(newTitle), + Label: manifold.Label(newName), } if projectDescription != "" { @@ -264,7 +257,7 @@ func updateProjectCmd(cliCtx *cli.Context) error { } spin.Stop() - fmt.Printf("\nYour project \"%s\" has been updated\n", newProjectTitle) + fmt.Printf("\nYour project \"%s\" has been updated\n", newTitle) return nil } @@ -295,7 +288,7 @@ func deleteProjectCmd(cliCtx *cli.Context) error { return err } - p, err := selectProject(ctx, projectName, teamID, client.Marketplace) + p, err := selectProject(ctx, projectName, teamID, client.Marketplace, true) if err != nil { return err } @@ -398,7 +391,7 @@ func addProjectCmd(cliCtx *cli.Context) error { return cli.NewExitError(fmt.Sprintf("Failed to fetch projects list: %s", err), -1) } - p, err := selectProject(ctx, projectName, teamID, client.Marketplace) + p, err := selectProject(ctx, projectName, teamID, client.Marketplace, true) if err != nil { return err } @@ -637,7 +630,7 @@ func updateResourceProject(ctx context.Context, uid, tid *manifold.ID, r *mModel } // selectProject prompts a user to select a project (if selects the one provided automatically) -func selectProject(ctx context.Context, projectName string, teamID *manifold.ID, marketplaceClient *mClient.Marketplace) (*mModels.Project, error) { +func selectProject(ctx context.Context, projectName string, teamID *manifold.ID, marketplaceClient *mClient.Marketplace, showResult bool) (*mModels.Project, error) { projects, err := clients.FetchProjects(ctx, marketplaceClient, teamID) if err != nil { return nil, cli.NewExitError(fmt.Sprintf("Failed to fetch list of projects: %s", err), -1) @@ -647,7 +640,7 @@ func selectProject(ctx context.Context, projectName string, teamID *manifold.ID, return nil, errs.ErrNoProjects } - idx, _, err := prompts.SelectProject(projects, projectName, false) + idx, _, err := prompts.SelectProject(projects, projectName, false, showResult) if err != nil { return nil, prompts.HandleSelectError(err, "Could not select project") } @@ -655,9 +648,3 @@ func selectProject(ctx context.Context, projectName string, teamID *manifold.ID, p := projects[idx] return p, nil } - -// generateName makes a title lowercase and replace spaces with dashes -func generateName(title string) manifold.Label { - name := strings.Replace(strings.ToLower(title), " ", "-", -1) - return manifold.Label(name) -} diff --git a/cmd/teams.go b/cmd/teams.go index 826e2fc..2f02946 100644 --- a/cmd/teams.go +++ b/cmd/teams.go @@ -101,7 +101,7 @@ func createTeamCmd(cliCtx *cli.Context) error { return err } - teamTitle, err := optionalArgTitle(cliCtx, 0, "team") + teamName, teamTitle, err := promptNameAndTitle(cliCtx, "team", true, false) if err != nil { return err } @@ -111,13 +111,7 @@ func createTeamCmd(cliCtx *cli.Context) error { return err } - autoSelect := teamTitle != "" - teamTitle, err = prompts.TeamTitle(teamTitle, autoSelect) - if err != nil { - return prompts.HandleSelectError(err, "Failed to name team") - } - - if err := createTeam(ctx, teamTitle, client.Identity); err != nil { + if err := createTeam(ctx, teamName, teamTitle, client.Identity); err != nil { return cli.NewExitError(fmt.Sprintf("Could not create team: %s", err), -1) } @@ -137,11 +131,6 @@ func updateTeamCmd(cliCtx *cli.Context) error { return err } - newTeamTitle, err := validateTitle(cliCtx, "title", "team") - if err != nil { - return err - } - client, err := api.New(api.Identity) if err != nil { return err @@ -152,17 +141,20 @@ func updateTeamCmd(cliCtx *cli.Context) error { return err } - autoSelect := newTeamTitle != "" - newTeamTitle, err = prompts.TeamTitle(newTeamTitle, autoSelect) + providedTitle := cliCtx.String("title") + if providedTitle == "" { + providedTitle = string(team.Body.Name) + } + newName, newTitle, err := createNameAndTitle(cliCtx, "team", string(team.Body.Label), providedTitle, true, false, false) if err != nil { - return prompts.HandleSelectError(err, "Could not validate name") + return err } - if err := updateTeam(ctx, team, newTeamTitle, client.Identity); err != nil { + if err := updateTeam(ctx, team, newName, newTitle, client.Identity); err != nil { return cli.NewExitError(fmt.Sprintf("Could not update team: %s", err), -1) } - fmt.Printf("Your team \"%s\" has been updated\n", newTeamTitle) + fmt.Printf("Your team \"%s\" has been updated\n", newTitle) return nil } @@ -302,7 +294,7 @@ func listTeamCmd(cliCtx *cli.Context) error { }) for _, team := range teams { - fmt.Fprintf(w, "%s\t%d\n", team.Name, team.Members) + fmt.Fprintf(w, "%s (%s)\t%d\n", team.Name, color.Faint(team.Title), team.Members) } return w.Flush() } @@ -354,11 +346,11 @@ func leaveTeamCmd(cliCtx *cli.Context) error { return nil } -func createTeam(ctx context.Context, teamTitle string, identityClient *client.Identity) error { +func createTeam(ctx context.Context, teamName, teamTitle string, identityClient *client.Identity) error { createTeam := &models.CreateTeam{ Body: &models.CreateTeamBody{ Name: manifold.Name(teamTitle), - Label: generateName(teamTitle), + Label: manifold.Label(teamName), }, } @@ -385,11 +377,11 @@ func createTeam(ctx context.Context, teamTitle string, identityClient *client.Id return nil } -func updateTeam(ctx context.Context, team *models.Team, teamTitle string, identityClient *client.Identity) error { +func updateTeam(ctx context.Context, team *models.Team, teamName, teamTitle string, identityClient *client.Identity) error { updateTeam := &models.UpdateTeam{ Body: &models.UpdateTeamBody{ Name: manifold.Name(teamTitle), - Label: generateName(teamTitle), + Label: manifold.Label(teamName), }, } diff --git a/cmd/update.go b/cmd/update.go index 7fb7743..b0283af 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -96,22 +96,18 @@ func updateResourceCmd(cliCtx *cli.Context) error { resource = resources[idx] } - newTitle := cliCtx.String("title") - title := string(resource.Body.Name) - autoSelect := false - if newTitle != "" { - title = newTitle - autoSelect = true + providedTitle := cliCtx.String("title") + if providedTitle == "" { + providedTitle = string(resource.Body.Name) } - - newTitle, err = prompts.ResourceName(title, autoSelect) + newName, newTitle, err := createNameAndTitle(cliCtx, "resource", string(resource.Body.Label), providedTitle, true, false, false) if err != nil { cli.NewExitError(fmt.Sprintf("Could not rename the resource: %s", err), -1) } prompts.SpinStart(fmt.Sprintf("Updating resource %q", resource.Body.Label)) - mrb, err := updateResource(ctx, resource, client.Marketplace, newTitle) + mrb, err := updateResource(ctx, resource, client.Marketplace, newName, newTitle) if err != nil { return cli.NewExitError(fmt.Sprintf("Failed to update resource: %s", err), -1) } @@ -137,11 +133,11 @@ func pickResourcesByName(resources []*models.Resource, name string) (*models.Res } func updateResource(ctx context.Context, r *models.Resource, - marketplaceClient *client.Marketplace, resourceName string) (*models.Resource, error) { + marketplaceClient *client.Marketplace, resourceName string, resourceTitle string) (*models.Resource, error) { rename := &models.PublicUpdateResource{ Body: &models.PublicUpdateResourceBody{ - Name: manifold.Name(resourceName), - Label: generateName(resourceName), + Name: manifold.Name(resourceTitle), + Label: manifold.Label(resourceName), }, } diff --git a/prompts/prompts.go b/prompts/prompts.go index 27aa89f..8a1b017 100644 --- a/prompts/prompts.go +++ b/prompts/prompts.go @@ -26,23 +26,24 @@ const NumberMask = '#' var errBad = errors.New("Bad Value") -// ResourceTitle prompts the user to provide a resource title or to accept empty -// to let the system generate one. -func ResourceTitle(defaultValue string, autoSelect bool) (string, error) { +// Title prompts the user to provide a Title value +func Title(field, defaultValue string, autoSelect, allowEmpty bool) (string, error) { + field = strings.Title(field) + validate := func(input string) error { - if len(input) == 0 { + if allowEmpty && len(input) == 0 { return nil } t := manifold.Name(input) if err := t.Validate(nil); err != nil { - return errors.New("Please provide a valid resource title") + return fmt.Errorf("Please provide a valid %s title", field) } return nil } - label := "Resource Title (one will be generated if left blank)" + label := fmt.Sprintf("New %s Title", field) if autoSelect { err := validate(defaultValue) @@ -63,22 +64,24 @@ func ResourceTitle(defaultValue string, autoSelect bool) (string, error) { return p.Run() } -// ResourceName prompts the user to provide a label name -func ResourceName(defaultValue string, autoSelect bool) (string, error) { +// Name prompts the user to provide a Name value +func Name(field, defaultValue string, autoSelect, allowEmpty bool) (string, error) { + field = strings.Title(field) + validate := func(input string) error { if len(input) == 0 { - return errors.New("Please provide a resource name") + return fmt.Errorf("Please provide a %s name", field) } l := manifold.Label(input) if err := l.Validate(nil); err != nil { - return errors.New("Please provide a valid resource name") + return fmt.Errorf("Please provide a valid %s name", field) } return nil } - label := "Resource Name" + label := fmt.Sprintf("New %s Name", field) if autoSelect { err := validate(defaultValue) @@ -100,76 +103,25 @@ func ResourceName(defaultValue string, autoSelect bool) (string, error) { return p.Run() } -// TeamTitle prompts the user to enter a new Team title -func TeamTitle(defaultValue string, autoSelect bool) (string, error) { - validate := func(input string) error { - if len(input) == 0 { - return errors.New("Please provide a valid team title") - } - - l := manifold.Name(input) - if err := l.Validate(nil); err != nil { - return errors.New("Please provide a valid team title") - } - - return nil - } - - label := "Team Title" - - if autoSelect { - err := validate(defaultValue) - if err != nil { - fmt.Println(templates.PromptFailure(label, defaultValue)) - } else { - fmt.Println(templates.PromptSuccess(label, defaultValue)) - } - return defaultValue, err - } +// ResourceTitle prompts the user to provide a resource title or to accept empty +// to let the system generate one. +func ResourceTitle(defaultValue string, autoSelect bool) (string, error) { + return Title("resource", defaultValue, autoSelect, true) +} - p := promptui.Prompt{ - Label: label, - Default: defaultValue, - Validate: validate, - } +// ResourceName prompts the user to provide a label name +func ResourceName(defaultValue string, autoSelect bool) (string, error) { + return Name("resource", defaultValue, autoSelect, false) +} - return p.Run() +// TeamTitle prompts the user to enter a new Team title +func TeamTitle(defaultValue string, autoSelect bool) (string, error) { + return Title("team", defaultValue, autoSelect, false) } // ProjectTitle prompts the user to enter a new project title func ProjectTitle(defaultValue string, autoSelect bool) (string, error) { - validate := func(input string) error { - if len(input) == 0 { - return errors.New("Please provide a valid project title") - } - - l := manifold.Name(input) - if err := l.Validate(nil); err != nil { - return errors.New("Please provide a valid project title") - } - - return nil - } - - label := "Project Title" - - if autoSelect { - err := validate(defaultValue) - if err != nil { - fmt.Println(templates.PromptFailure(label, defaultValue)) - } else { - fmt.Println(templates.PromptSuccess(label, defaultValue)) - } - return defaultValue, err - } - - p := promptui.Prompt{ - Label: label, - Default: defaultValue, - Validate: validate, - } - - return p.Run() + return Title("project", defaultValue, autoSelect, false) } // TokenDescription prompts the user to enter a token description diff --git a/prompts/selects.go b/prompts/selects.go index 7861c03..020df90 100644 --- a/prompts/selects.go +++ b/prompts/selects.go @@ -154,7 +154,7 @@ func SelectRegion(list []*cModels.Region) (int, string, error) { } // SelectProject prompts the user to select a project from the given list. -func SelectProject(list []*mModels.Project, name string, emptyOption bool) (int, string, error) { +func SelectProject(list []*mModels.Project, name string, emptyOption, showResult bool) (int, string, error) { projects := templates.Projects(list) tpls := templates.TplProject @@ -175,8 +175,10 @@ func SelectProject(list []*mModels.Project, name string, emptyOption bool) (int, return 0, "", errs.ErrProjectNotFound } - msg := templates.SelectSuccess(tpls, projects[idx]) - fmt.Println(msg) + if showResult { + msg := templates.SelectSuccess(tpls, projects[idx]) + fmt.Println(msg) + } return idx, name, nil }