From fbd016c2fe4a6052cd6b2cc4423cbeb82703810e Mon Sep 17 00:00:00 2001 From: Sven Greb Date: Tue, 17 Nov 2020 10:32:29 +0100 Subject: [PATCH] Abstract "task" API: spell incantation, kind and caster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "wand" API is inspired by the fantasy novel "Harry Potter" [1] and uses an abstract view to define interfaces. The main motivation to create a matching naming to the overall "magic" topic and the actual target project Mage [2]. This might be too abstract for some, but is kept understandable insofar as it should allow everyone to use the "task" API and to derive their own tasks from it. - `cast.Caster` - A `interface` type that casts a `spell.Incantation` using a command for a specific `spell.Kind`: - `Cast(spell.Incantation) error` - casts a spell incantation. - `Handles() spell.Kind` - returns the spell kind that can be casted. - `Validate() error` - validates the caster command. - `cast.BinaryCaster` - A `interface` type that composes `cast.Caster` to run commands using a binary executable: - `GetExec() string` - returns the path to the binary executable of the command. - `spell.Incantation` - A `interface` type that is the abstract representation of parameters for a command or action: - `Formula() []string` - returns all parameters of a spell. - `Kind() Kind` - returns the Kind of a spell. - `Options() interface{}` - return the options of a spell. - `cast.Binary` - A `interface` type that composes `cast.Caster` for commands which are using a binary executable: - `Env() map[string]string` - returns additional environment variables. - `cast.GoCode` - A `interface` type that composes `cast.Caster` for actions that can be casted without a `cast.Caster`: - `Cast() (interface{}, error)` - casts itself - `cast.GoModule` - A `interface` type that composes `cast.Binary` for commands that are compiled from a [Go module][3]: - `GoModuleID() *project.GoModuleID` - returns the identifier of a Go module. - `spell.Kind` - A `struct` type that defines the kind of a spell. The API components can be roughly translated to their purpose: - `cast.Caster` -> a executable command It validates the command and defines which `spell.Kind` can be handled by this caster. It could be executed without parameters (`spell.Incantation`), but in most cases needs at least one parameter. - `cast.BinaryCaster` -> a composed `cast.Caster` to run commands using a binary executable. It ensures that the executable file exists and stores information like the path. It could also be executed without parameters (`spell.Incantation`), but would not have any effect im many cases. - `spell.Incantation` -> the parameters of a executable command It assemble all parameters based on the given options and ensures the they are correctly formatted for the execution in a shell environment. Except for special incantations like `spell.GoCode` a incantation cannot be used alone but must be passed to a `cast.Caster` that is able to handle the `spell.Kind` of this incantation. - `spell.Binary` -> a composed `spell.Incantation` to run commands that are using binary executable. It can inject or override environment variables in the shell environment in which the the command will be run. - `spell.GoCode` -> a composed `spell.Incantation` for pure Go code instead of a (binary) executable command. It can “cast itself“, e.g. to simply delete a directory using packages like `os` from the Go standard library. It has been designed this way to also allow such tasks to be handled by the incantation API. - `spell.GoModule` -> a composed `spell.Binary` to run binary commands managed by a [Go module][3], in other words executables installed in `GOBIN` or received via `go get`. It requires the module identifier (`path@version`) in order to download and run the executable. [1]: https://en.wikipedia.org/wiki/Harry_Potter [2]: https://magefile.org [3]: https://golang.org/ref/mod GH-14 --- pkg/cast/cast.go | 36 +++++++++++++++++++++ pkg/spell/kind.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++ pkg/spell/spell.go | 72 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 pkg/cast/cast.go create mode 100644 pkg/spell/kind.go create mode 100644 pkg/spell/spell.go diff --git a/pkg/cast/cast.go b/pkg/cast/cast.go new file mode 100644 index 0000000..deedccd --- /dev/null +++ b/pkg/cast/cast.go @@ -0,0 +1,36 @@ +// Copyright (c) 2019-present Sven Greb +// This source code is licensed under the MIT license found in the LICENSE file. + +package cast + +import ( + "github.com/svengreb/wand/pkg/spell" +) + +// Caster casts a spell.Incantation using a command for a specific spell.Kind. +// +// The abstract view and naming is inspired by the fantasy novel "Harry Potter" in which a caster can cast a magic spell +// through a incantation. +// +// See +// +// (1) https://en.wikipedia.org/wiki/Magic_in_Harry_Potter#Spellcasting +// (2) https://en.wikipedia.org/wiki/Incantation +type Caster interface { + // Cast casts a spell incantation. + Cast(spell.Incantation) error + + // Handles returns the spell kind that can be casted. + Handles() spell.Kind + + // Validate validates the caster command. + Validate() error +} + +// BinaryCaster is a Caster to run commands using a binary executable. +type BinaryCaster interface { + Caster + + // GetExec returns the path to the binary executable of the command. + GetExec() string +} diff --git a/pkg/spell/kind.go b/pkg/spell/kind.go new file mode 100644 index 0000000..713d41c --- /dev/null +++ b/pkg/spell/kind.go @@ -0,0 +1,79 @@ +// Copyright (c) 2019-present Sven Greb +// This source code is licensed under the MIT license found in the LICENSE file. + +package spell + +import ( + "fmt" + "strings" +) + +const ( + // KindNameBinary is the Kind name for binary spells. + KindNameBinary = "binary" + // KindNameGoCode is the Kind name for Go code spells. + KindNameGoCode = "go.code" + // KindNameGoModule is the Kind name for Go module spells. + KindNameGoModule = "go.module" + // KindNameUnknown is the name for a unknown spell Kind. + KindNameUnknown = "unknown" +) + +const ( + // KindBinary is the Kind for binary spells. + KindBinary Kind = iota + // KindGoCode is the Kind for Go code spells. + KindGoCode + // KindGoModule is the Kind for Go module spells. + KindGoModule +) + +// Kind defines the kind of a spell. +type Kind uint32 + +// MarshalText returns the textual representation of itself. +func (k Kind) MarshalText() ([]byte, error) { + switch k { + case KindBinary: + return []byte(KindNameBinary), nil + case KindGoCode: + return []byte(KindNameGoCode), nil + case KindGoModule: + return []byte(KindNameGoModule), nil + } + + return nil, fmt.Errorf("not a valid kind %d", k) +} + +func (k Kind) String() string { + if b, err := k.MarshalText(); err == nil { + return string(b) + } + return KindNameUnknown +} + +// UnmarshalText implements encoding.TextUnmarshaler to unmarshal a textual representation of itself. +func (k *Kind) UnmarshalText(text []byte) error { + parsed, err := ParseKind(string(text)) + if err != nil { + return err + } + + *k = parsed + return nil +} + +// ParseKind takes a kind name and returns the Kind constant. +func ParseKind(name string) (Kind, error) { + switch strings.ToLower(name) { + case KindNameBinary: + return KindBinary, nil + case KindNameGoCode: + return KindGoCode, nil + case KindNameGoModule: + return KindGoModule, nil + } + + var k Kind + return k, fmt.Errorf("not a valid kind: %q", name) +} diff --git a/pkg/spell/spell.go b/pkg/spell/spell.go new file mode 100644 index 0000000..3aae97b --- /dev/null +++ b/pkg/spell/spell.go @@ -0,0 +1,72 @@ +// Copyright (c) 2019-present Sven Greb +// This source code is licensed under the MIT license found in the LICENSE file. + +package spell + +import "github.com/svengreb/wand/pkg/project" + +// Incantation is the abstract representation of parameters for a command or action. +// It is mainly handled by a cast.Caster that provides the corresponding information about the command like the path +// to the executable. +// +// The separation of parameters from commands enables a flexible usage, e.g. when the parameters can be reused for a +// different command. +// +// The abstract view and naming is inspired by the fantasy novel "Harry Potter" in which it is almost only possible to +// cast a magic spell through a incantation. +// +// See +// +// (1) https://en.wikipedia.org/wiki/Incantation +// (2) https://en.wikipedia.org/wiki/Magic_in_Harry_Potter#Spellcasting +// (3) https://scifi.stackexchange.com/a/33234 +// (4) https://harrypotter.fandom.com/wiki/Spell +// (5) https://diffsense.com/diff/incantation/spell +type Incantation interface { + // Formula returns all parameters of a spell. + Formula() []string + + // Kind returns the Kind of a spell. + Kind() Kind + + // Options return the options of a spell. + Options() interface{} +} + +// Binary is a Incantation for commands which are using a binary executable. +type Binary interface { + Incantation + + // Env returns additional environment variables. + Env() map[string]string +} + +// GoCode is a Incantation for actions that can be casted without a cast.Caster. +// It is a special incantations in that it allows to use Go code as spell while still being compatible to the +// incantation API. +// Note that the Incantation.Formula of a GoCode must always return an empty slice, +// otherwise it is a "normal" Incantation that requires a cast.Caster. +// +// Seen from the abstract "Harry Potter" view this is equal to a "non-verbal" spell that is a special technique that can +// be used for spells that have been specially designed to be used non-verbally. +// +// See +// +// (1) https://en.wikipedia.org/wiki/Magic_in_Harry_Potter#Spellcasting +// (2) https://www.reddit.com/r/harrypotter/comments/4z9rwl/what_is_the_difference_between_a_spell_charm +type GoCode interface { + Incantation + + // Cast casts itself. + Cast() (interface{}, error) +} + +// GoModule is a Binary for binary command executables managed by a Go module. +// +// See https://golang.org/ref/mod for more details. +type GoModule interface { + Binary + + // GoModuleID returns the identifier of a Go module. + GoModuleID() *project.GoModuleID +}