otium

package module
v0.1.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 29, 2023 License: MIT Imports: 14 Imported by: 0

README

otium: incremental automation of manual procedures

Otium attempts to solve the dilemma of procedures that are tedious, time-consuming and error-prone to perform manually but that are impractical to automate right now: maybe you don't have time, cannot yet see the justification, don't know yet the best approach.

The idea is to automate incrementally, starting from an empty code skeleton that substitutes the text document that you would normally write.

Instead of reading the document, you run the skeleton, which guides you step-by-step through the whole procedure. It is a simple REPL that gathers inputs from you and passes them to the subsequent steps.

When you have time and enough experience from proceeding manually (🤠), you automate (🤖) one step. When you have some more time, you automate another step, thus incrementally automating the whole procedure over time.

Scope

Otium aims to stay as simple as possible, because it is threading a fine line between manual procedures guided by a document and full automation with code.

In particular, there is no plan to support conditional logic, because I have the feeling that when the complexity of the procedure requires conditional logic, it is time to "just" write a "normal" Go program instead, with the full power of the language (we do not want to invent yet another DSL).

Status

  • Pre v1: assume API will change in a non-backward compatible way until v1.
  • Before a breaking change a version tag will bet set.

Example: WRITEME

Examples in the source code

See directory examples.

Usage

Write your own main and import this package.

Run the program. It has a REPL with command completion and history, thanks to peterh/liner. Enter ? or <TAB> twice to get started:

(top) Enter a command or '?' for help
(top)>> ?

Commands:
  help [<command> ...]
    Show help.

  repl
    Show help for the REPL.

  list
    Show the list of steps.

  next
    Run the next step.

  quit
    Quit the program.

  variables
    List the variables.

(top)>>

Printing the document instead of running

Invoke the otium procedure with --doc-only.

Setting a bag value from the command line

Sometimes you know beforehand some of the variables that the procedure steps will ask. In those cases, it can be simpler to pass them as command-line flags, instead of waiting to be prompted for them.

All the bag variables declared in a step are automatically made available as command-line flags. If present, the same validation function is used when parsing the command-line and when parsing the REPL input:

pcd.AddStep(&otium.Step{
    Title: "...",
    Desc: `...`,
    Vars: []otium.Variable{
        {
            Name: "fruit",
            Desc: "Fruit for breakfast",
            Fn: func(val string) error {
                basket := []string{"banana", "mango"}
                if !slices.Contains(basket, val) {
                    return fmt.Errorf("we only have %s", basket)
                }
                return nil
            },
        },
        {Name: "amount", Desc: "How many pieces of fruit"},
    },

Invoking help:

$ go run ./examples/cliflags -h
cliflags: Example showing command-line flags.
This program is based on otium dev, a simple incremental automation system (https://github.com/marco-m/otium)

Usage of cliflags:
  -amount value
        How many pieces of fruit
  -fruit value
        Fruit for breakfast

See examples/cliflags.

Understanding if a step is automated or manual

  • Manual steps are marked as a human with 🤠
  • Automated steps are marked as a bot with 🤖

Example:

go run ./examples/cliflags

# Example showing command-line flags

## Table of contents

next->  1. 🤠 Introduction
        2. 🤖 Two variables

Rendering bag values in the step description with Go template

Assuming that the procedure bag contains the k/v name: Joe, then

pcd.AddStep(&otium.Step{
    Desc: `Hello {{.name}}!`
})

will be rendered as:

Hello Joe!

This feature is inspired by danslimmon/donothing.

Returning an error from a step

Sometimes an error is recoverable within the same execution, sometimes it is unrecoverable.

If the error is recoverable, return an error as you please (wrapped or not), for example:

pcd.AddStep(&otium.Step{
    Run: func (bag otium.Bag) error {
        ...
        return fmt.Errorf("failed to X... %s", err)
    },
})

If the error is unrecoverable, use the w verb of fmt.Errorf (or equivalent):

pcd.AddStep(&otium.Step{
    Run: func (bag otium.Bag) error {
        ...
        return fmt.Errorf("failed to X... %w", otium.ErrUnrecoverable)
    },
})

Support for pre-flight checks user context

Sometimes you need to do one or both of the following:

  1. Perform procedure-specific validations before running the steps, for example ensuring that a specific environment variable is set. Nevertheless, you still want the procedure to return normal help usage if invoked with --help.
  2. Initialize and then pass to each step a user context, for example a struct representing an API client.

This is achieved as follows.

  • Set field PostCli of otium.ProcedureOpts to your callback function, that performs the validations you need and returns an initialized user context.
      PostCli: func() (any, error) {
          return foo.NewClient(), nil
      },
    
  • In the callback,
  • At each step, in the Run function, perform a type assert and get your user context:
      Run: func(bag otium.Bag, uctx any) error {
          // Type assertion (https://go.dev/tour/methods/15)
          fooClient := uctx.(*foo.Client)
      }
    

See examples/usercontext for a complete example.

Design decisions

  • To test the interactive behavior, I wrote a minimal expect package, inspired by the great Expect Tcl.
  • Cannot inject io.Reader, io.Writer to ease testing because the REPL library has hardcoded os.Stdin and os.Stdout. Instead, we use expect.NewFilePipe() and swap os.Stdin, os.Stdout in each test.

Credits

The idea of otium comes from danslimmon/donothing at v0.2.0, which had the rationale that impressed me and the majority of preliminary code, but was still missing support for running the user-provided automation functions.

License

MIT.

Documentation

Overview

Package otium allows incremental automation of manual procedures

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnrecoverable tells the REPL to exit.
	//
	// From client code, use the %w verb of fmt.Errorf:
	// 	func(bag otium.Bag) error {
	//	    return fmt.Errorf("failed to X... %w", otium.ErrUnrecoverable)
	//	},
	ErrUnrecoverable = errors.New("(unrecoverable)")
)

Functions

This section is empty.

Types

type Bag

type Bag struct {
	// contains filtered or unexported fields
}

Bag is passed to the [RunFn] of Step. It contains all the k/v pairs added by the various steps during the execution of the otium Procedure.

func NewBag added in v0.1.5

func NewBag() Bag

func (*Bag) Get

func (bag *Bag) Get(key string) (string, error)

Get returns the value of key if key exists. If key doesn't exist, Get returns an error.

In case of error, it means that you didn't set the Variables field of Procedure.AddStep. See the examples for clarification.

func (*Bag) Put

func (bag *Bag) Put(key, val string)

Put adds key/val to bag, overwriting val if key already exists.

type Procedure

type Procedure struct {
	ProcedureOpts
	// contains filtered or unexported fields
}

Procedure is made of a sequence of Step. Create it with NewProcedure, fill it with Procedure.AddStep and finally start it with Procedure.Execute.

func NewProcedure

func NewProcedure(opts ProcedureOpts) *Procedure

NewProcedure creates a Procedure.

func (*Procedure) AddStep

func (pcd *Procedure) AddStep(step *Step)

AddStep adds a Step to Procedure.

func (*Procedure) Execute

func (pcd *Procedure) Execute(args []string) error

Execute parses the command-line passed in args, assigns bag variables and starts the Procedure by putting the user into a REPL. If it returns an error, the user program should print it and exit with a non-zero status code. See the examples for the suggested usage.

func (*Procedure) Put added in v0.1.5

func (pcd *Procedure) Put(key, val string)

type ProcedureOpts

type ProcedureOpts struct {
	// Name is the name of the Procedure; by default it is the name of the executable.
	Name string
	// Title is the title of the Procedure, shown at the beginning of the program.
	Title string
	// Desc is the summary of what the procedure is about, shown at the beginning of
	// the program, just after the Title.
	Desc string
	// PreFlight is an optional function run after the command-line has been parsed and
	// before the first step.
	// It is used to perform procedure-specific validations and to initialize the user
	// context. Such user context will then be passed as parameter uctx to each call
	// of Step.Run(bag Bag, uctx any).
	PreFlight func() (any, error)
}

ProcedureOpts is used by NewProcedure to create a Procedure.

type Step

type Step struct {
	// Title is the name of the step, shown in table of contents and at the
	// beginning of the step itself.
	Title string
	// Desc is the detailed description of what a human is supposed to do to
	// perform the step. If the step is automated, Desc should be shortened
	// and adapted to the change.
	Desc string
	// Vars are the new variables needed by the step.
	Vars []Variable
	// Run is the optional automation of the step. If the step is manual,
	// leave Run unset. When called, bag will contain all the key/value pairs
	// set by the previous steps and uctx, if not nil, will point to the user
	// context.
	// A typical Run will use [Bag.Get] to get a k/v, [Bag.Put] to put a k/v and
	// will type assert uctx to the type returned by [ProcedureOpts.PreFlight].
	// For the user context, see also [ProcedureOpts.PreFlight] and
	// examples/usercontext.
	Run func(bag Bag, uctx any) error
}

Step is part of a Procedure. See [Procedure.Add].

func (*Step) Icon added in v0.1.5

func (step *Step) Icon() string

type ValidatorFn added in v0.1.2

type ValidatorFn func(val string) error

ValidatorFn is the optional function to validate a k/v pair. It is called either when parsing the command-line or when processing the Vars field of a Step.

type Variable added in v0.1.5

type Variable struct {
	Name string
	Desc string
	Fn   ValidatorFn
	// contains filtered or unexported fields
}

Variable is an item of Bag. Field Name is also the key in [Get]: if key "foo" exists, then: foo, _ := bag.Get("foo") foo.Name == "foo"

Directories

Path Synopsis
examples
phonex8
Based on the example at https://github.com/danslimmon/donothing/tree/main/example
Based on the example at https://github.com/danslimmon/donothing/tree/main/example

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL