Skip to content

Commit

Permalink
Merge pull request #139 from choria-io/137
Browse files Browse the repository at this point in the history
(#137) adds a scaffold command
  • Loading branch information
ripienaar authored Aug 29, 2023
2 parents 55f3a47 + c74aa2a commit 10a469f
Show file tree
Hide file tree
Showing 176 changed files with 10,452 additions and 7,383 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ jobs:
test:
strategy:
matrix:
go: [ "1.19", "1.20" ]
go: [ "1.20", "1.21" ]

runs-on: ubuntu-latest
steps:
Expand Down
92 changes: 73 additions & 19 deletions ABTaskFile
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,82 @@ commands:
go mod tidy

- name: test
type: exec
description: Perform unit tests
dir: "{{ TaskDir }}"
arguments:
- name: dir
description: Directory to test
default: .
flags:
- name: update
description: Updates the ginkgo runtime
bool: true
script: |
set -e
type: parent
aliases: [t]
description: Perform various tests
commands:
- name: unit
type: exec
description: Run ginkgo unit tests
aliases: [u]
arguments:
- name: dir
description: Directory to test
default: .
flags:
- name: update
description: Updates the ginkgo runtime
bool: true
script: |
set -e

. "{{ BashHelperPath }}"

. "{{ BashHelperPath }}"
{{ if .Flags.update }}
ab_say Updating ginkgo binary
go install github.com/onsi/ginkgo/v2/ginkgo
{{ end }}

{{ if .Flags.update }}
ab_say Updating ginkgo binary
go install github.com/onsi/ginkgo/v2/ginkgo
{{ end }}
ginkgo -r --skip Integration {{ .Arguments.dir | escape }}

ginkgo -r --skip Integration {{ .Arguments.dir | escape }}
- name: lint
type: exec
dir: "{{ AppDir }}"
flags:
- name: vet
description: Perform go vet
bool: true
default: true
- name: staticcheck
description: Perform staticcheck
bool: true
default: true
- name: update
description: Updates lint dependencies
bool: true
script: |
set -e

. "{{ BashHelperPath }}"

{{ if .Flags.update }}
ab_say Updating linting tools
go install github.com/client9/misspell/cmd/misspell@latest
go install honnef.co/go/tools/cmd/staticcheck@latest
{{ else }}
echo ">>> Run with --update to install required commands"
echo
{{ end }}

ab_say Formatting source files
go fmt ./...

ab_say Tidying go mod
go mod tidy

ab_say Checking spelling
find . -type f -name "*.go" | xargs misspell -error -locale US -i flavour
find docs/content -type f -name "*.md" | xargs misspell -error -locale US

{{ if .Flags.vet }}
ab_say Performing go vet
go vet ./...
{{ end }}

{{ if .Flags.staticcheck }}
ab_say Running staticcheck
staticcheck ./...
{{ end }}


- name: docs
Expand Down
2 changes: 1 addition & 1 deletion builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type Command interface {
String() string
}

type templateState struct {
type TemplateState struct {
Arguments any
Flags any
Config any
Expand Down
4 changes: 4 additions & 0 deletions builder/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ type Logger interface {
// Default console logger
type defaultLogger struct{}

func NewDefaultLogger() Logger {
return &defaultLogger{}
}

func (l *defaultLogger) Infof(format string, v ...any) {
log.Printf(format, v...)
}
Expand Down
15 changes: 9 additions & 6 deletions builder/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func dereferenceArgsOrFlags(input map[string]any) map[string]any {
return res
}

func templateFuncs(all bool) template.FuncMap {
func TemplateFuncs(all bool) template.FuncMap {
funcs := map[string]any{}
if all {
funcs = sprig.TxtFuncMap()
Expand Down Expand Up @@ -88,15 +88,18 @@ func templateFuncs(all bool) template.FuncMap {
return funcs
}

// ParseStateTemplateWithFuncMap parses body as a go text template with supplied values exposed to the user with additional functions available to the template
func ParseStateTemplateWithFuncMap(body string, args map[string]any, flags map[string]any, cfg any, funcMap template.FuncMap) (string, error) {
state := templateState{
func NewTemplateState(args map[string]any, flags map[string]any, cfg any, input any) *TemplateState {
return &TemplateState{
Arguments: dereferenceArgsOrFlags(args),
Flags: dereferenceArgsOrFlags(flags),
Config: cfg,
Input: input,
}
}

funcs := templateFuncs(false)
// ParseStateTemplateWithFuncMap parses body as a go text template with supplied values exposed to the user with additional functions available to the template
func ParseStateTemplateWithFuncMap(body string, args map[string]any, flags map[string]any, cfg any, funcMap template.FuncMap) (string, error) {
funcs := TemplateFuncs(false)
for n, f := range funcMap {
funcs[n] = f
}
Expand All @@ -107,7 +110,7 @@ func ParseStateTemplateWithFuncMap(body string, args map[string]any, flags map[s
}

var b bytes.Buffer
err = temp.Execute(&b, state)
err = temp.Execute(&b, NewTemplateState(args, flags, cfg, nil))
if err != nil {
return "", err
}
Expand Down
9 changes: 2 additions & 7 deletions builder/transform_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (tt *templateTransform) Transform(ctx context.Context, r io.Reader, args ma
}

templ := template.New("builder")
templ.Funcs(templateFuncs(true))
templ.Funcs(TemplateFuncs(true))

switch {
case tt.Source != "":
Expand All @@ -69,12 +69,7 @@ func (tt *templateTransform) Transform(ctx context.Context, r io.Reader, args ma
}

out := bytes.NewBuffer([]byte{})
state := templateState{
Arguments: dereferenceArgsOrFlags(args),
Flags: dereferenceArgsOrFlags(flags),
Config: cfg,
Input: input,
}
state := NewTemplateState(args, flags, cfg, input)

err = templ.Execute(out, state)
if err != nil {
Expand Down
153 changes: 153 additions & 0 deletions commands/scaffold/scaffold.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Copyright (c) 2023, R.I. Pienaar and the Choria Project contributors
//
// SPDX-License-Identifier: Apache-2.0

package scaffold

import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"strings"

"github.com/choria-io/appbuilder/builder"
"github.com/choria-io/appbuilder/scaffold"
"github.com/choria-io/fisk"
)

type Command struct {
Target string `json:"target"`
SourceDirectory string `json:"source_directory"`
Source map[string]any `json:"source"`
Post []map[string]string `json:"post"`

builder.GenericSubCommands
builder.GenericCommand
}

type Scaffold struct {
defnDir string
userDir string
arguments map[string]any
flags map[string]any
cmd *fisk.CmdClause
def *Command
ctx context.Context
log builder.Logger
b *builder.AppBuilder
}

var (
ErrorInvalidConfiguration = errors.New("invalid configuration")
ErrRenderFailed = errors.New("render failed")
)

func Register() error {
return builder.RegisterCommand("scaffold", NewScaffoldCommand)
}

func MustRegister() {
builder.MustRegisterCommand("scaffold", NewScaffoldCommand)
}

func NewScaffoldCommand(b *builder.AppBuilder, j json.RawMessage, log builder.Logger) (builder.Command, error) {
s := &Scaffold{
def: &Command{},
ctx: b.Context(),
defnDir: b.DefinitionDirectory(),
userDir: b.UserWorkingDirectory(),
b: b,
log: log,
arguments: map[string]any{},
flags: map[string]any{},
}

err := json.Unmarshal(j, s.def)
if err != nil {
return nil, fmt.Errorf("%w: %v", builder.ErrInvalidDefinition, err)
}

return s, nil
}

func (r *Scaffold) String() string { return fmt.Sprintf("%s (scaffold)", r.def.Name) }

func (r *Scaffold) Validate(log builder.Logger) error {
if r.def.Type != "scaffold" {
return fmt.Errorf("not an scaffold command")
}

var errs []string

if r.def.Target == "" {
errs = append(errs, "target is required")
}

if len(r.def.Source) == 0 && r.def.SourceDirectory == "" {
errs = append(errs, "no sources provided")
}

if r.def.SourceDirectory != "" {
_, err := os.Stat(r.def.SourceDirectory)
if err != nil {
errs = append(errs, fmt.Sprintf("cannot read source directory: %v", err))
}
}

if _, err := os.Stat(r.def.Target); !os.IsNotExist(err) {
errs = append(errs, "target directory exist")
}

if len(errs) > 0 {
return errors.New(strings.Join(errs, ", "))
}

return nil
}

func (r *Scaffold) SubCommands() []json.RawMessage {
return r.def.Commands
}

func (r *Scaffold) CreateCommand(app builder.KingpinCommand) (*fisk.CmdClause, error) {
r.cmd = builder.CreateGenericCommand(app, &r.def.GenericCommand, r.arguments, r.flags, r.b, r.runCommand)

return r.cmd, nil
}

func (r *Scaffold) runCommand(_ *fisk.ParseContext) error {
cfg := scaffold.Config{
Source: r.def.Source,
Post: r.def.Post,
}

var err error

if cfg.SourceDirectory != "" {
cfg.SourceDirectory, err = builder.ParseStateTemplate(r.def.SourceDirectory, r.arguments, r.flags, r.b.Configuration())
if err != nil {
return err
}
}

cfg.TargetDirectory, err = builder.ParseStateTemplate(r.def.Target, r.arguments, r.flags, r.b.Configuration())
if err != nil {
return err
}

s, err := scaffold.New(cfg, builder.TemplateFuncs(true))
if err != nil {
return fmt.Errorf("%w: %w", ErrorInvalidConfiguration, err)
}

s.Logger(builder.NewDefaultLogger())

err = s.Render(builder.NewTemplateState(r.arguments, r.flags, r.b.Configuration(), nil))
if err != nil {
return fmt.Errorf("%w: %w", ErrRenderFailed, err)
}

return nil
}
2 changes: 1 addition & 1 deletion docs/content/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,6 @@ In the example above one can run commands like `natsctl report servers` or `nats

The `natscl service state` command invokes a Choria RPC API in a subset of fleet nodes and pass the result through a JQ query `.replies | .[] | select(.statuscode==0) | .sender + ": " + .data.state` to transform the API output.

It is much nicer to just wrap it in `natsctl report servers` or `natsctl service state` and be able to manage the detail seperately. You can mention `natsctl report servers` in wikis and if it ever changes, you only change the app model.
It is much nicer to just wrap it in `natsctl report servers` or `natsctl service state` and be able to manage the detail separately. You can mention `natsctl report servers` in wikis and if it ever changes, you only change the app model.

These sub commands all have help and integration with bash and zsh is provided.
Loading

0 comments on commit 10a469f

Please sign in to comment.