Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Designing Pluggable Idiomatic Go Applications – Mark Bates

Designing Pluggable Idiomatic Go Applications – Mark Bates

GopherCon Russia

March 28, 2020
Tweet

More Decks by GopherCon Russia

Other Decks in Programming

Transcript

  1. Usage: buffalo [command] Available Commands: build Build the application binary,

    including bundling of assets (packr & webpack) db [PLUGIN] [DEPRECATED] please use `buffalo pop` instead. destroy Destroy generated components dev Run the Buffalo app in 'development' mode fix Attempt to fix a Buffalo application's API to match version v0.15.3 generate Generate application components help Help about any command heroku [PLUGIN] helps with heroku setup and deployment for buffalo applications info Print diagnostic information (useful for debugging) new Creates a new Buffalo application plugins tools for working with buffalo plugins pop [PLUGIN] A tasty treat for all your database needs routes Print all defined routes setup Setup a newly created, or recently checked out application. task Run grift tasks test Run the tests for the Buffalo app. Use --force-migrations to skip schema load. version Print the version information Context $ buffalo -h
  2. Problems •Dependencies - modules, packages, etc… •Hard-coded tools - Pop,

    Webpack, Plush, etc… •Spaghetti code, poorly tested, etc… •Incorrect generated output/Mis-matched versions
  3. Plugins •Executables named like buffalo-<plugin-name> •in $BUFFALO_PLUGIN_PATH •in $GOPATH/bin •in

    ./plugins •Implement an available command returning JSON plugin information
  4. Problems •Slow •Discovery •Caching •Hard to write/use •No versioning of

    plugins/Version mismatch •Still can’t replace or access key components •…
  5. Requirements •The CLI and plugins need to be in go.mod

    •The application should control the whole CLI experience •Plugins need to be simple Go •Little to no magic •Independent with minimal dependencies (standard library)
  6. Auto-Detecting CLI $ buffalo setup arg1 arg2 If ./cmd/buffalo/main.go Else

    print help $ go run ./cmd/buffalo/main.go setup arg1 arg2 Process with local CLI/plugins
  7. func main() { fmt.Print("~~~~ Using coke/cmd/buffalo ~~~\n\n") ctx := context.Background()

    pwd, err := os.Getwd() if err != nil { log.Fatal(err) } buffalo, err := cli.New() if err != nil { log.Fatal(err) } // append your plugins here // buffalo.Plugins = append(buffalo.Plugins, ...) err = buffalo.Main(ctx, pwd, os.Args[1:]) if err != nil { log.Fatal(err) } } Local CLI ./cmd/buffalo/main.go
  8. buffalo.Main(ctx, pwd, os.Args[1:]) A Callable API ./cmd/buffalo/main.go type Buffalo struct

    { Plugins []plugins.Plugin } func (b *Buffalo) Main(ctx context.Context, root string, args []string) error github.com/gobuffalo/buffalo-cli/v2/cli#Buffalo
  9. Hand-waving the Solution •Plugins and CLI versioned in go.mod •Developers

    can create unique toolsets for each app •Can compile cmd/buffalo and have an app specific Buffalo binary
 
 $ go build -o bin/buffalo ./cmd/buffalo
 $ ./bin/buffalo build arg1 arg2
  10. func main() { ctx := context.Background() pwd, _ := os.Getwd()

    buffalo := &cli.Buffalo{} err := buffalo.Main(ctx, pwd, os.Args[1:]) if err != nil { log.Fatal(err) } } Zero Value Tools for working with Buffalo applications $ buffalo --------- Using Plugins: Type Name Description ---- ---- ----------- *cli.Buffalo buffalo Tools for working with Buffalo applications
  11. Eating the Dog Food •Every sub-command is a plugin •Core

    and 3rd party plugins use the same API •No private APIs
  12. Small and Concise •Interfaces should be 1-2 methods •Interfaces should

    try and use standard library types •Plugins define their own eco-system •Plugins should only expose a few interfaces
  13. The Life of a Subcommand type Commander interface { plugins.Plugin

    Main(ctx context.Context, root string, args []string) error } github.com/gobuffalo/buffalo-cli/v2/cli#Commander
  14. The Setup Plugin type Cmd struct {} func (Cmd) PluginName()

    string func (cmd *Cmd) Main(ctx context.Context, root string, args []string) error github.com/gobuffalo/buffalo-cli/v2/cli/cmds/setup#Cmd
  15. Setting up as a Service $ buffalo setup Before Setup

    Install Dependencies Validate System Setup Create/Migrate Databases Install Webpack Dependencies Start Background Services After Setup Run tests Report Errors
  16. type BeforeSetuper interface { plugins.Plugin BeforeSetup(ctx context.Context, root string, args

    []string) error } type Setuper interface { plugins.Plugin Setup(ctx context.Context, root string, args []string) error } type AfterSetuper interface { plugins.Plugin AfterSetup(ctx context.Context, root string, args []string, err error) error } Setupers github.com/gobuffalo/buffalo-cli/v2/cli/cmds/setup
  17. func (cmd *Cmd) Main(ctx context.Context, root string, args []string) error

    { err := cmd.run(ctx, root, args) return cmd.afterSetup(ctx, root, args, err) } func (cmd *Cmd) run(ctx context.Context, root string, args []string) error { if err := cmd.beforeSetup(ctx, root, args); err != nil { return err } for _, p := range cmd.ScopedPlugins() { if s, ok := p.(Setuper); ok { if err := s.Setup(ctx, root, args); err != nil { return err } } } return nil } Setting up for Success! github.com/gobuffalo/buffalo-cli/v2/cli/cmds/setup#Cmd.Main
  18. func (cmd *Cmd) beforeSetup(ctx context.Context, root string, args []string) error

    { plugs := cmd.ScopedPlugins() for _, p := range plugs { if bb, ok := p.(BeforeSetuper); ok { if err := bb.BeforeSetup(ctx, root, args); err != nil { return err } } } return nil } func (cmd *Cmd) afterSetup(ctx context.Context, root string, args []string, err error) error { plugs := cmd.ScopedPlugins() for _, p := range plugs { if bb, ok := p.(AfterSetuper); ok { if err := bb.AfterSetup(ctx, root, args, err); err != nil { return err } } } return err } Before and After github.com/gobuffalo/buffalo-cli/v2/cli/cmds/setup#Cmd
  19. type Setup struct {} func (Setup) PluginName() string func (s

    *Setup) Setup(ctx context.Context, root string, args []string) error Pop Goes the Plugin github.com/gobuffalo/buffalo-cli/v2/cli/internal/plugins/pop/setup#Setup type Migrater interface { MigrateDB(ctx context.Context, root string, args []string) error } type DBSeeder interface { SeedDB(ctx context.Context, root string, args []string) error }
  20. // Scoper can be implemented to return a slice of

    plugins that // are important to the type defining it. type Scoper interface { ScopedPlugins() []Plugin } type Feeder func() []Plugin // Needer can be implemented to receive a Feeder function // that can be used to gain access to other plugins in the system. type Needer interface { WithPlugins(Feeder) } Needing and Feeding github.com/gobuffalo/plugins
  21. var _ BeforeSetuper = beforeSetuper(nil) type beforeSetuper func(ctx context.Context, root

    string, args []string) error func (b beforeSetuper) PluginName() string { return "before-setuper" } func (b beforeSetuper) BeforeSetup(ctx context.Context, root string, args []string) error { return b(ctx, root, args) } Testing Types