github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/runners/hello/hello_example.go (about) 1 // Package hello provides "hello command" logic and associated types. By 2 // convention, the construction function for the primary type (also named 3 // "hello") is simply named "New". For other construction functions, the name 4 // of the type is used as a suffix. For instance, "hello.NewInfo()" for the 5 // type "hello.Info". Similarly, "Info" would be used as a prefix or suffix for 6 // the info-related types like "InfoRunParams". Each "group of types" is usually 7 // expressed in its own file. 8 package hello 9 10 import ( 11 "errors" 12 13 "github.com/ActiveState/cli/internal/errs" 14 "github.com/ActiveState/cli/internal/locale" 15 "github.com/ActiveState/cli/internal/output" 16 "github.com/ActiveState/cli/internal/primer" 17 "github.com/ActiveState/cli/internal/runbits" 18 "github.com/ActiveState/cli/internal/runbits/rationalize" 19 "github.com/ActiveState/cli/pkg/localcommit" 20 "github.com/ActiveState/cli/pkg/platform/authentication" 21 "github.com/ActiveState/cli/pkg/platform/model" 22 "github.com/ActiveState/cli/pkg/project" 23 ) 24 25 // primeable describes the app-level dependencies that a runner will need. 26 type primeable interface { 27 primer.Outputer 28 primer.Auther 29 primer.Projecter 30 } 31 32 // Params defines the parameters needed to execute a given runner. These 33 // values are typically collected from flags and arguments entered into the 34 // cli, but there is no reason that they couldn't be set in another manner. 35 type Params struct { 36 Name string 37 Echo Text 38 Extra bool 39 } 40 41 // NewParams contains a scope in which default or construction-time values 42 // can be set. If no default or construction-time values are necessary, direct 43 // construction of Params is fine, and this construction func may be dropped. 44 func NewParams() *Params { 45 return &Params{} 46 } 47 48 // Hello defines the app-level dependencies that are accessible within the Run 49 // function. 50 type Hello struct { 51 out output.Outputer 52 project *project.Project 53 auth *authentication.Auth 54 } 55 56 // New contains the scope in which an instance of Hello is constructed from an 57 // implementation of primeable. 58 func New(p primeable) *Hello { 59 return &Hello{ 60 out: p.Output(), 61 project: p.Project(), 62 auth: p.Auth(), 63 } 64 } 65 66 // rationalizeError is used to interpret the returned error and rationalize it for the end-user. 67 // This is so that end-users always get errors that clearly relate to what they were doing, with a good sense on what 68 // they can do to address it. 69 func rationalizeError(err *error) { 70 switch { 71 case err == nil: 72 return 73 case errs.Matches(*err, &runbits.NoNameProvidedError{}): 74 // Errors that we are looking for should be wrapped in a user-facing error. 75 // Ensure we wrap the top-level error returned from the runner and not 76 // the unpacked error that we are inspecting. 77 *err = errs.WrapUserFacing(*err, locale.Tl("hello_err_no_name", "Cannot say hello because no name was provided.")) 78 case errors.Is(*err, rationalize.ErrNoProject): 79 // It's useful to offer users reasonable tips on recourses. 80 *err = errs.WrapUserFacing( 81 *err, 82 locale.Tl("hello_err_no_project", "Cannot say hello because you are not in a project directory."), 83 errs.SetTips( 84 locale.Tl("hello_suggest_checkout", "Try using '[ACTIONABLE]state checkout[/RESET]' first."), 85 ), 86 ) 87 } 88 } 89 90 // Run contains the scope in which the hello runner logic is executed. 91 func (h *Hello) Run(params *Params) (rerr error) { 92 defer rationalizeError(&rerr) 93 94 h.out.Print(locale.Tl("hello_notice", "This command is for example use only")) 95 96 if h.project == nil { 97 return rationalize.ErrNoProject 98 } 99 100 // Reusable runner logic is contained within the runbits package. 101 // You should only use this if you intend to share logic between 102 // runners. Runners should NEVER invoke other runners. 103 if err := runbits.SayHello(h.out, params.Name); err != nil { 104 // Errors should nearly always be localized. 105 return errs.Wrap( 106 err, "Cannot say hello.", 107 ) 108 } 109 110 if params.Echo.IsSet() { 111 h.out.Print(locale.Tl( 112 "hello_echo_msg", "Echoing: {{.V0}}", 113 params.Echo.String(), 114 )) 115 } 116 117 if !params.Extra { 118 return nil 119 } 120 121 // Grab data from the platform. 122 commitMsg, err := currentCommitMessage(h.project, h.auth) 123 if err != nil { 124 err = errs.Wrap( 125 err, "Cannot get commit message", 126 ) 127 return errs.AddTips( 128 err, 129 locale.Tl("hello_info_suggest_ensure_commit", "Ensure project has commits"), 130 ) 131 } 132 133 h.out.Print(locale.Tl( 134 "hello_extra_info", 135 "Project: {{.V0}}\nCurrent commit message: {{.V1}}", 136 h.project.Namespace().String(), commitMsg, 137 )) 138 139 return nil 140 } 141 142 // currentCommitMessage contains the scope in which the current commit message 143 // is obtained. Since it is a sort of construction function that has some 144 // complexity, it is helpful to provide localized error context. Secluding this 145 // sort of logic is helpful to keep the subhandlers clean. 146 func currentCommitMessage(proj *project.Project, auth *authentication.Auth) (string, error) { 147 if proj == nil { 148 return "", errs.New("Cannot determine which project to use") 149 } 150 151 commitId, err := localcommit.Get(proj.Dir()) 152 if err != nil { 153 return "", errs.Wrap(err, "Cannot determine which commit to use") 154 } 155 156 commit, err := model.GetCommit(commitId, auth) 157 if err != nil { 158 return "", errs.Wrap(err, "Cannot get commit from server") 159 } 160 161 commitMsg := locale.Tl("hello_info_warn_no_commit", "Commit description not provided.") 162 if commit.Message != "" { 163 commitMsg = commit.Message 164 } 165 166 return commitMsg, nil 167 }