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  }