andrey1s
4 years ago
commit
0bd6f67397
80 changed files with 4741 additions and 0 deletions
@ -0,0 +1,24 @@ |
|||||
|
kind: pipeline |
||||
|
name: default |
||||
|
|
||||
|
steps: |
||||
|
- name: golangci-lint |
||||
|
image: golangci/golangci-lint:v1.26 |
||||
|
volumes: |
||||
|
- name: deps |
||||
|
path: /go/src/mod |
||||
|
commands: |
||||
|
- golangci-lint run --timeout 5m |
||||
|
|
||||
|
- name: test |
||||
|
image: golang |
||||
|
volumes: |
||||
|
- name: deps |
||||
|
path: /go/src/mod |
||||
|
commands: |
||||
|
- go test ./... |
||||
|
|
||||
|
volumes: |
||||
|
- name: deps |
||||
|
temp: {} |
||||
|
|
@ -0,0 +1,17 @@ |
|||||
|
# ---> Go |
||||
|
# Binaries for programs and plugins |
||||
|
*.exe |
||||
|
*.exe~ |
||||
|
*.dll |
||||
|
*.so |
||||
|
*.dylib |
||||
|
|
||||
|
# Test binary, built with `go test -c` |
||||
|
*.test |
||||
|
|
||||
|
# Output of the go coverage tool, specifically when used with LiteIDE |
||||
|
*.out |
||||
|
|
||||
|
# Dependency directories (remove the comment below to include it) |
||||
|
# vendor/ |
||||
|
|
@ -0,0 +1,36 @@ |
|||||
|
linters-settings: |
||||
|
dupl: |
||||
|
threshold: 100 |
||||
|
funlen: |
||||
|
lines: 100 |
||||
|
statements: 50 |
||||
|
goconst: |
||||
|
min-len: 2 |
||||
|
min-occurrences: 2 |
||||
|
gocyclo: |
||||
|
min-complexity: 15 |
||||
|
golint: |
||||
|
min-confidence: 0 |
||||
|
gomnd: |
||||
|
settings: |
||||
|
mnd: |
||||
|
# don't include the "operation" and "assign" |
||||
|
checks: argument,case,condition,return |
||||
|
govet: |
||||
|
check-shadowing: true |
||||
|
lll: |
||||
|
line-length: 140 |
||||
|
maligned: |
||||
|
suggest-new: true |
||||
|
misspell: |
||||
|
locale: US |
||||
|
|
||||
|
linters: |
||||
|
enable-all: true |
||||
|
|
||||
|
issues: |
||||
|
# Excluding configuration per-path, per-linter, per-text and per-source |
||||
|
exclude-rules: |
||||
|
- path: _test\.go |
||||
|
linters: |
||||
|
- gomnd |
@ -0,0 +1,19 @@ |
|||||
|
MIT License Copyright (c) 2020 go-4devs |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
|
of this software and associated documentation files (the "Software"), to deal |
||||
|
in the Software without restriction, including without limitation the rights |
||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
|
copies of the Software, and to permit persons to whom the Software is furnished |
||||
|
to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice (including the next |
||||
|
paragraph) shall be included in all copies or substantial portions of the |
||||
|
Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS |
||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS |
||||
|
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
||||
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF |
||||
|
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
@ -0,0 +1,187 @@ |
|||||
|
# Console |
||||
|
|
||||
|
## Creating a Command |
||||
|
|
||||
|
Commands are defined in struct extending `pkg/command/create_user.go`. For example, you may want a command to create a user: |
||||
|
|
||||
|
```go |
||||
|
package command |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console" |
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/output" |
||||
|
) |
||||
|
|
||||
|
func Createuser() *console.Command { |
||||
|
return &console.Command{ |
||||
|
Name: "app:create-user", |
||||
|
Execute: func(ctx context.Context, in input.Input, out output.Output) error { |
||||
|
return nil |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
## Configure command |
||||
|
|
||||
|
```go |
||||
|
func Createuser() *console.Command { |
||||
|
return &console.Command{ |
||||
|
//... |
||||
|
Description: "Creates a new user.", |
||||
|
Help: "This command allows you to create a user...", |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
## Add arguments |
||||
|
|
||||
|
```go |
||||
|
func Createuser(required bool) *console.Command { |
||||
|
return &console.Command{ |
||||
|
//.... |
||||
|
Configure: func(ctx context.Context, cfg *input.Definition) error { |
||||
|
var opts []func(*input.Argument) |
||||
|
if required { |
||||
|
opts = append(opts, argument.Required) |
||||
|
} |
||||
|
cfg.SetArgument("password", "User password", opts...) |
||||
|
|
||||
|
return nil |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Registering the Command |
||||
|
|
||||
|
`cmd/console/main.go` |
||||
|
|
||||
|
```go |
||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console" |
||||
|
"pkg/command" |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
console. |
||||
|
New(). |
||||
|
Add( |
||||
|
command.Createuser(false), |
||||
|
). |
||||
|
Execute(context.Background()) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Executing the Command |
||||
|
|
||||
|
build command `go build -o bin/console cmd/console/main.go` |
||||
|
run command `bin/console app:create-user`` |
||||
|
|
||||
|
## Console Output |
||||
|
|
||||
|
The Execute field has access to the output stream to write messages to the console: |
||||
|
```go |
||||
|
func Createuser(required bool) *console.Command { |
||||
|
return &console.Command{ |
||||
|
// .... |
||||
|
Execute: func(ctx context.Context, in input.Input, out output.Output) error { |
||||
|
// outputs a message followed by a "\n" |
||||
|
out.Println(ctx, "User Creator") |
||||
|
out.Println(ctx, "Whoa!") |
||||
|
|
||||
|
// outputs a message without adding a "\n" at the end of the line |
||||
|
out.Print(ctx, "You are about to ", "create a user.") |
||||
|
|
||||
|
return nil |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Now, try build and executing the command: |
||||
|
|
||||
|
```bash |
||||
|
bin/console app:create-user |
||||
|
User Creator |
||||
|
Whoa! |
||||
|
You are about to create a user. |
||||
|
``` |
||||
|
|
||||
|
## Console Input |
||||
|
|
||||
|
Use input options or arguments to pass information to the command: |
||||
|
|
||||
|
```go |
||||
|
func CreateUser(required bool) *console.Command { |
||||
|
return &console.Command{ |
||||
|
Configure: func(ctx context.Context, cfg *input.Definition) error { |
||||
|
var opts []func(*input.Argument) |
||||
|
if required { |
||||
|
opts = append(opts, argument.Required) |
||||
|
} |
||||
|
cfg. |
||||
|
SetArgument("username", "The username of the user.", argument.Required). |
||||
|
SetArgument("password", "User password", opts...) |
||||
|
|
||||
|
return nil |
||||
|
}, |
||||
|
Execute: func(ctx context.Context, in input.Input, out output.Output) error { |
||||
|
// outputs a message followed by a "\n" |
||||
|
out.Println(ctx, "User Creator") |
||||
|
out.Println(ctx, "Username: ", in.Argument(ctx, "username").String()) |
||||
|
|
||||
|
return nil |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Now, you can pass the username to the command: |
||||
|
|
||||
|
```bash |
||||
|
bin/console app:create-user AwesomeUsername |
||||
|
User Creator |
||||
|
Username: AwesomeUsername |
||||
|
``` |
||||
|
|
||||
|
## Testing Commands |
||||
|
|
||||
|
```go |
||||
|
package command_test |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"context" |
||||
|
"testing" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console" |
||||
|
"gitoa.ru/go-4devs/console/example/pkg/command" |
||||
|
"gitoa.ru/go-4devs/console/input/array" |
||||
|
"gitoa.ru/go-4devs/console/output/writer" |
||||
|
) |
||||
|
|
||||
|
func TestCreateUser(t *testing.T) { |
||||
|
ctx := context.Background() |
||||
|
in := array.New(array.Argument("username", "andrey")) |
||||
|
buf := bytes.Buffer{} |
||||
|
out := writer.Buffer(&buf) |
||||
|
|
||||
|
console.Run(ctx, command.CreateUser(false), in, out) |
||||
|
|
||||
|
expect := `User Creator |
||||
|
Username: andrey |
||||
|
` |
||||
|
|
||||
|
if expect != buf.String() { |
||||
|
t.Errorf("expect: %s, got:%s", expect, buf.String()) |
||||
|
} |
||||
|
} |
||||
|
``` |
@ -0,0 +1,141 @@ |
|||||
|
package console |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"os" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/input/argv" |
||||
|
"gitoa.ru/go-4devs/console/input/value" |
||||
|
"gitoa.ru/go-4devs/console/output" |
||||
|
"gitoa.ru/go-4devs/console/output/writer" |
||||
|
) |
||||
|
|
||||
|
// WithOutput sets outpu,^ by default output os.Stdout.
|
||||
|
func WithOutput(out output.Output) func(*App) { |
||||
|
return func(a *App) { |
||||
|
a.out = out |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// WithInput sets input, by default creates inpur by os.Args.
|
||||
|
func WithInput(in input.Input) func(*App) { |
||||
|
return func(a *App) { |
||||
|
a.in = in |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// WithSkipArgs sets how many arguments are passed. For example, you don't need to pass the name of a single command.
|
||||
|
func WithSkipArgs(l int) func(*App) { |
||||
|
return func(a *App) { |
||||
|
a.skipArgv = l |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// WithExit sets exit callback by default os.Exit.
|
||||
|
func WithExit(f func(int)) func(*App) { |
||||
|
return func(a *App) { |
||||
|
a.exit = f |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// New creates and configure new console app.
|
||||
|
func New(opts ...func(*App)) *App { |
||||
|
a := &App{ |
||||
|
out: writer.Stdout(), |
||||
|
exit: os.Exit, |
||||
|
} |
||||
|
|
||||
|
for _, opt := range opts { |
||||
|
opt(a) |
||||
|
} |
||||
|
|
||||
|
if a.in == nil { |
||||
|
skip := 2 |
||||
|
|
||||
|
switch { |
||||
|
case a.skipArgv > 0 && len(os.Args) > a.skipArgv: |
||||
|
skip = a.skipArgv |
||||
|
case a.skipArgv > 0: |
||||
|
skip = len(os.Args) |
||||
|
case len(os.Args) == 1: |
||||
|
skip = 1 |
||||
|
case len(os.Args) > 1 && os.Args[1][0] == '-': |
||||
|
skip = 1 |
||||
|
} |
||||
|
|
||||
|
a.in = argv.New(os.Args[skip:]) |
||||
|
} |
||||
|
|
||||
|
return a |
||||
|
} |
||||
|
|
||||
|
// App is collection of command and configure env.
|
||||
|
type App struct { |
||||
|
cmds []*Command |
||||
|
out output.Output |
||||
|
in input.Input |
||||
|
skipArgv int |
||||
|
exit func(int) |
||||
|
} |
||||
|
|
||||
|
// Add add or replace command.
|
||||
|
func (a *App) Add(cmds ...*Command) *App { |
||||
|
a.cmds = append(a.cmds, cmds...) |
||||
|
|
||||
|
return a |
||||
|
} |
||||
|
|
||||
|
// Execute run the command by name and arguments.
|
||||
|
func (a *App) Execute(ctx context.Context) { |
||||
|
for _, cmd := range a.cmds { |
||||
|
register(cmd) |
||||
|
} |
||||
|
|
||||
|
cmd, err := a.find(ctx) |
||||
|
if err != nil { |
||||
|
a.printError(ctx, err) |
||||
|
|
||||
|
if err := a.list(ctx); err != nil { |
||||
|
a.printError(ctx, err) |
||||
|
} |
||||
|
|
||||
|
a.exit(1) |
||||
|
} |
||||
|
|
||||
|
a.exec(ctx, cmd) |
||||
|
} |
||||
|
|
||||
|
func (a *App) exec(ctx context.Context, cmd *Command) { |
||||
|
if err := Run(ctx, cmd, a.in, a.out); err != nil { |
||||
|
a.printError(ctx, err) |
||||
|
a.exit(1) |
||||
|
} |
||||
|
|
||||
|
a.exit(0) |
||||
|
} |
||||
|
|
||||
|
func (a *App) find(_ context.Context) (*Command, error) { |
||||
|
if len(os.Args) < 2 || os.Args[1][1] == '-' { |
||||
|
return Find(CommandList) |
||||
|
} |
||||
|
|
||||
|
name := os.Args[1] |
||||
|
|
||||
|
return Find(name) |
||||
|
} |
||||
|
|
||||
|
func (a *App) list(ctx context.Context) error { |
||||
|
cmd, err := Find(CommandHelp) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
a.in.SetArgument("command_name", value.New(CommandList)) |
||||
|
|
||||
|
return Run(ctx, cmd, a.in, a.out) |
||||
|
} |
||||
|
|
||||
|
func (a *App) printError(ctx context.Context, err error) { |
||||
|
a.out.Println(ctx, "<error>\n\n ", err, "\n</error>") |
||||
|
} |
@ -0,0 +1,87 @@ |
|||||
|
package console_test |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"os" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console" |
||||
|
"gitoa.ru/go-4devs/console/example/pkg/command" |
||||
|
) |
||||
|
|
||||
|
//nolint: lll
|
||||
|
func ExampleNew_help() { |
||||
|
ctx := context.Background() |
||||
|
os.Args = []string{ |
||||
|
"bin/console", |
||||
|
"test:command", |
||||
|
"-h", |
||||
|
"--no-ansi", |
||||
|
} |
||||
|
|
||||
|
console.New(console.WithExit(func(int) {})). |
||||
|
Add( |
||||
|
Command(), |
||||
|
). |
||||
|
Execute(ctx) |
||||
|
// Output:
|
||||
|
// Description:
|
||||
|
// test command
|
||||
|
//
|
||||
|
// Usage:
|
||||
|
// test:command [options] [--] [<test_argument>]
|
||||
|
//
|
||||
|
// Arguments:
|
||||
|
// test_argument test argument
|
||||
|
//
|
||||
|
// Options:
|
||||
|
// --duration[=DURATION] test duration with default [default: 1s]
|
||||
|
// --bool test bool option
|
||||
|
// --string[=STRING] array string (multiple values allowed)
|
||||
|
// -q, --quiet Do not output any message
|
||||
|
// -v, --verbose Increase the verbosity of messages: -v for info output, -vv for debug and -vvv for trace (multiple values allowed)
|
||||
|
// -h, --help Display this help message
|
||||
|
// -V, --version Display this application version
|
||||
|
// --ansi Do not ask any interactive question
|
||||
|
// --no-ansi Disable ANSI output
|
||||
|
} |
||||
|
|
||||
|
func ExampleNew_list() { |
||||
|
ctx := context.Background() |
||||
|
|
||||
|
os.Args = []string{ |
||||
|
"bin/console", |
||||
|
"--no-ansi", |
||||
|
} |
||||
|
|
||||
|
console.New(console.WithExit(func(int) {})). |
||||
|
Add( |
||||
|
Command(), |
||||
|
command.Hello(), |
||||
|
command.Args(), |
||||
|
command.Namespace(), |
||||
|
). |
||||
|
Execute(ctx) |
||||
|
// Output:
|
||||
|
// Usage:
|
||||
|
// command [options] [arguments]
|
||||
|
//
|
||||
|
// Options:
|
||||
|
// -q, --quiet Do not output any message
|
||||
|
// -v, --verbose Increase the verbosity of messages: -v for info output, -vv for debug and -vvv for trace (multiple values allowed)
|
||||
|
// -h, --help Display this help message
|
||||
|
// -V, --version Display this application version
|
||||
|
// --ansi Do not ask any interactive question
|
||||
|
// --no-ansi Disable ANSI output
|
||||
|
//
|
||||
|
// Available commands:
|
||||
|
// help Displays help for a command
|
||||
|
// list Lists commands
|
||||
|
// app
|
||||
|
// app:start example command in other namespace
|
||||
|
// fdevs
|
||||
|
// fdevs:console:arg Understanding how Console Arguments and Options Are Handled
|
||||
|
// fdevs:console:hello example hello command
|
||||
|
// fdevs:console:test test command
|
||||
|
// test
|
||||
|
// test:command test command
|
||||
|
} |
@ -0,0 +1,196 @@ |
|||||
|
package console |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"fmt" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/output" |
||||
|
) |
||||
|
|
||||
|
type ( |
||||
|
Action func(ctx context.Context, input input.Input, output output.Output) error |
||||
|
Handle func(ctx context.Context, in input.Input, out output.Output, n Action) error |
||||
|
Configure func(ctx context.Context, cfg *input.Definition) error |
||||
|
Prepare func(ctx context.Context, cfg *input.Definition, n Configure) error |
||||
|
Option func(*Command) |
||||
|
) |
||||
|
|
||||
|
// WithPrepare append middleware for configuration command.
|
||||
|
func WithPrepare(p ...Prepare) Option { |
||||
|
return func(c *Command) { |
||||
|
if c.Prepare != nil { |
||||
|
p = append([]Prepare{c.Prepare}, p...) |
||||
|
} |
||||
|
|
||||
|
c.Prepare = ChainPrepare(p...) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// WithHandle append middleware for executed command.
|
||||
|
func WithHandle(h ...Handle) Option { |
||||
|
return func(c *Command) { |
||||
|
if c.Handle != nil { |
||||
|
h = append([]Handle{c.Handle}, h...) |
||||
|
} |
||||
|
|
||||
|
c.Handle = ChainHandle(h...) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// WithHidden sets hidden command.
|
||||
|
func WithHidden(v bool) Option { |
||||
|
return func(c *Command) { |
||||
|
c.Hidden = v |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// WithName sets name command.
|
||||
|
func WithName(n string) Option { |
||||
|
return func(c *Command) { |
||||
|
c.Name = n |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
type Command struct { |
||||
|
// The name of the command.
|
||||
|
Name string |
||||
|
// A short description of the usage of this command.
|
||||
|
Description string |
||||
|
// A longer explanation of how the command works.
|
||||
|
Help string |
||||
|
// Vervion command.
|
||||
|
Version string |
||||
|
// Boolean to hide this command from help or completion.
|
||||
|
Hidden bool |
||||
|
// Configures the current command.
|
||||
|
Configure Configure |
||||
|
// The middleware for configures current command.
|
||||
|
Prepare Prepare |
||||
|
// The function to call when this command is invoked.
|
||||
|
Execute Action |
||||
|
// The middleware for executes current command.
|
||||
|
Handle Handle |
||||
|
} |
||||
|
|
||||
|
func (c *Command) String() string { |
||||
|
return fmt.Sprintf("name: %s, version: %s", c.Name, c.Version) |
||||
|
} |
||||
|
|
||||
|
// With creates new command by parent and options.
|
||||
|
func (c *Command) With(opts ...Option) *Command { |
||||
|
cmd := &Command{ |
||||
|
Name: c.Name, |
||||
|
Description: c.Description, |
||||
|
Help: c.Help, |
||||
|
Version: c.Version, |
||||
|
Hidden: c.Hidden, |
||||
|
Configure: c.Configure, |
||||
|
Prepare: c.Prepare, |
||||
|
Execute: c.Execute, |
||||
|
Handle: c.Handle, |
||||
|
} |
||||
|
|
||||
|
for _, opt := range opts { |
||||
|
opt(cmd) |
||||
|
} |
||||
|
|
||||
|
return cmd |
||||
|
} |
||||
|
|
||||
|
// Run run command with input and output.
|
||||
|
func (c *Command) Run(ctx context.Context, in input.Input, out output.Output) error { |
||||
|
if c.Handle != nil { |
||||
|
return c.Handle(ctx, in, out, c.Execute) |
||||
|
} |
||||
|
|
||||
|
return c.Execute(ctx, in, out) |
||||
|
} |
||||
|
|
||||
|
// Init configures command.
|
||||
|
func (c *Command) Init(ctx context.Context, cfg *input.Definition) error { |
||||
|
switch { |
||||
|
case c.Prepare != nil && c.Configure != nil: |
||||
|
return c.Prepare(ctx, cfg, c.Configure) |
||||
|
case c.Prepare != nil: |
||||
|
return c.Prepare(ctx, cfg, func(_ context.Context, _ *input.Definition) error { |
||||
|
return nil |
||||
|
}) |
||||
|
case c.Configure != nil: |
||||
|
return c.Configure(ctx, cfg) |
||||
|
default: |
||||
|
return nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ChainPrepare creates middleware for configures command.
|
||||
|
func ChainPrepare(prepare ...Prepare) Prepare { |
||||
|
n := len(prepare) |
||||
|
if n == 1 { |
||||
|
return prepare[0] |
||||
|
} |
||||
|
|
||||
|
if n > 1 { |
||||
|
lastI := n - 1 |
||||
|
|
||||
|
return func(ctx context.Context, def *input.Definition, next Configure) error { |
||||
|
var ( |
||||
|
chainHandler func(context.Context, *input.Definition) error |
||||
|
curI int |
||||
|
) |
||||
|
|
||||
|
chainHandler = func(currentCtx context.Context, currentDef *input.Definition) error { |
||||
|
if curI == lastI { |
||||
|
return next(currentCtx, currentDef) |
||||
|
} |
||||
|
curI++ |
||||
|
err := prepare[curI](currentCtx, currentDef, chainHandler) |
||||
|
curI-- |
||||
|
|
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
return prepare[0](ctx, def, chainHandler) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return func(ctx context.Context, cfg *input.Definition, next Configure) error { |
||||
|
return next(ctx, cfg) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// ChainHandle creates middleware for executes command.
|
||||
|
func ChainHandle(handlers ...Handle) Handle { |
||||
|
n := len(handlers) |
||||
|
if n == 1 { |
||||
|
return handlers[0] |
||||
|
} |
||||
|
|
||||
|
if n > 1 { |
||||
|
lastI := n - 1 |
||||
|
|
||||
|
return func(ctx context.Context, in input.Input, out output.Output, next Action) error { |
||||
|
var ( |
||||
|
chainHandler func(context.Context, input.Input, output.Output) error |
||||
|
curI int |
||||
|
) |
||||
|
|
||||
|
chainHandler = func(currentCtx context.Context, currentIn input.Input, currentOut output.Output) error { |
||||
|
if curI == lastI { |
||||
|
return next(currentCtx, currentIn, currentOut) |
||||
|
} |
||||
|
curI++ |
||||
|
err := handlers[curI](currentCtx, currentIn, currentOut, chainHandler) |
||||
|
curI-- |
||||
|
|
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
return handlers[0](ctx, in, out, chainHandler) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return func(ctx context.Context, in input.Input, out output.Output, next Action) error { |
||||
|
return next(ctx, in, out) |
||||
|
} |
||||
|
} |
@ -0,0 +1,122 @@ |
|||||
|
package console_test |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"strings" |
||||
|
"sync/atomic" |
||||
|
"testing" |
||||
|
"time" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console" |
||||
|
"gitoa.ru/go-4devs/console/example/pkg/command" |
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/input/array" |
||||
|
"gitoa.ru/go-4devs/console/input/option" |
||||
|
"gitoa.ru/go-4devs/console/output" |
||||
|
"gitoa.ru/go-4devs/console/output/writer" |
||||
|
) |
||||
|
|
||||
|
//nolint: gochecknoinits
|
||||
|
func init() { |
||||
|
console.MustRegister(Command().With(console.WithName("fdevs:console:test"))) |
||||
|
console.MustRegister(command.Args()) |
||||
|
} |
||||
|
|
||||
|
func Command() *console.Command { |
||||
|
return &console.Command{ |
||||
|
Name: "test:command", |
||||
|
Description: "test command", |
||||
|
Execute: func(ctx context.Context, in input.Input, out output.Output) error { |
||||
|
out.Print(ctx, |
||||
|
"test argument:", in.Argument(ctx, "test_argument").String(), "\n", |
||||
|
"bool option:", in.Option(ctx, "bool").Bool(), "\n", |
||||
|
"duration option with default:", in.Option(ctx, "duration").Duration(), "\n", |
||||
|
"array string:[", strings.Join(in.Option(ctx, "string").Strings(), ","), "]\n", |
||||
|
) |
||||
|
|
||||
|
return nil |
||||
|
}, |
||||
|
Configure: func(ctx context.Context, def *input.Definition) error { |
||||
|
def. |
||||
|
SetArguments( |
||||
|
input.NewArgument("test_argument", "test argument"), |
||||
|
). |
||||
|
SetOptions( |
||||
|
input.NewOption("string", "array string", option.Array), |
||||
|
option.Bool("bool", "test bool option"), |
||||
|
option.Duration("duration", "test duration with default", option.Default(time.Second)), |
||||
|
) |
||||
|
|
||||
|
return nil |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestChainPrepare(t *testing.T) { |
||||
|
var cnt int32 |
||||
|
|
||||
|
ctx := context.Background() |
||||
|
def := input.NewDefinition() |
||||
|
|
||||
|
prepare := func(ctx context.Context, def *input.Definition, n console.Configure) error { |
||||
|
atomic.AddInt32(&cnt, 1) |
||||
|
|
||||
|
return n(ctx, def) |
||||
|
} |
||||
|
configure := func(context.Context, *input.Definition) error { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
for i := range []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10} { |
||||
|
prepares := make([]console.Prepare, i) |
||||
|
for p := 0; p < i; p++ { |
||||
|
prepares[p] = prepare |
||||
|
} |
||||
|
|
||||
|
cnt = 0 |
||||
|
chain := console.ChainPrepare(prepares...) |
||||
|
|
||||
|
if err := chain(ctx, def, configure); err != nil { |
||||
|
t.Errorf("expected nil err, got: %s", err) |
||||
|
} |
||||
|
|
||||
|
if cnt != int32(i) { |
||||
|
t.Fatalf("expected: call prepare 1, got: %d ", cnt) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestChainHandle(t *testing.T) { |
||||
|
var cnt int32 |
||||
|
|
||||
|
ctx := context.Background() |
||||
|
in := array.New() |
||||
|
out := writer.Stdout() |
||||
|
|
||||
|
handle := func(ctx context.Context, in input.Input, out output.Output, next console.Action) error { |
||||
|
atomic.AddInt32(&cnt, 1) |
||||
|
|
||||
|
return next(ctx, in, out) |
||||
|
} |
||||
|
action := func(context.Context, input.Input, output.Output) error { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
for i := range []int{0, 1, 2, 30, 40, 50} { |
||||
|
handles := make([]console.Handle, i) |
||||
|
for p := 0; p < i; p++ { |
||||
|
handles[p] = handle |
||||
|
} |
||||
|
|
||||
|
cnt = 0 |
||||
|
chain := console.ChainHandle(handles...) |
||||
|
|
||||
|
if err := chain(ctx, in, out, action); err != nil { |
||||
|
t.Errorf("expected nil err, got: %s", err) |
||||
|
} |
||||
|
|
||||
|
if cnt != int32(i) { |
||||
|
t.Fatalf("expected: call prepare 1, got: %d ", cnt) |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,135 @@ |
|||||
|
package console |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"errors" |
||||
|
"os" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/input/option" |
||||
|
"gitoa.ru/go-4devs/console/input/value" |
||||
|
"gitoa.ru/go-4devs/console/output" |
||||
|
"gitoa.ru/go-4devs/console/output/verbosity" |
||||
|
"gitoa.ru/go-4devs/console/output/wrap" |
||||
|
) |
||||
|
|
||||
|
const ( |
||||
|
verboseTrace = 3 |
||||
|
verboseDebug = 2 |
||||
|
verboseInfo = 3 |
||||
|
) |
||||
|
|
||||
|
// Execute the current command with option.
|
||||
|
func Execute(ctx context.Context, cmd *Command, opts ...func(*App)) { |
||||
|
opts = append([]func(*App){WithSkipArgs(1)}, opts...) |
||||
|
New(opts...).exec(ctx, cmd) |
||||
|
} |
||||
|
|
||||
|
// Run current command by input and output/
|
||||
|
func Run(ctx context.Context, cmd *Command, in input.Input, out output.Output) error { |
||||
|
def := input.NewDefinition() |
||||
|
|
||||
|
if err := cmd.Init(ctx, def); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
if err := in.Bind(ctx, Default(def)); err != nil { |
||||
|
ansi(ctx, in, out).Print(ctx, "<error>\n\n ", err, "\n</error>\n") |
||||
|
|
||||
|
return showHelp(ctx, cmd, in, wrap.Ansi(out)) |
||||
|
} |
||||
|
|
||||
|
out = ansi(ctx, in, out) |
||||
|
|
||||
|
out = verbose(ctx, in, out) |
||||
|
|
||||
|
if in.Option(ctx, "version").Bool() { |
||||
|
version := cmd.Version |
||||
|
if version == "" { |
||||
|
version = "unknown" |
||||
|
} |
||||
|
|
||||
|
out.Println(ctx, "command <comment>", cmd.Name, "</comment> version: <info>", version, "</info>") |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
if in.Option(ctx, "help").Bool() { |
||||
|
return showHelp(ctx, cmd, in, out) |
||||
|
} |
||||
|
|
||||
|
return cmd.Run(ctx, in, out) |
||||
|
} |
||||
|
|
||||
|
func ansi(ctx context.Context, in input.Input, out output.Output) output.Output { |
||||
|
switch { |
||||
|
case in.Option(ctx, "ansi").Bool(): |
||||
|
out = wrap.Ansi(out) |
||||
|
case in.Option(ctx, "no-ansi").Bool(): |
||||
|
out = wrap.None(out) |
||||
|
case lookupEnv("NO_COLOR"): |
||||
|
out = wrap.None(out) |
||||
|
default: |
||||
|
out = wrap.Ansi(out) |
||||
|
} |
||||
|
|
||||
|
return out |
||||
|
} |
||||
|
|
||||
|
func lookupEnv(name string) bool { |
||||
|
v, has := os.LookupEnv(name) |
||||
|
|
||||
|
return has && v == "true" |
||||
|
} |
||||
|
|
||||
|
func verbose(ctx context.Context, in input.Input, out output.Output) output.Output { |
||||
|
switch { |
||||
|
case in.Option(ctx, "quiet").Bool(): |
||||
|
out = verbosity.Quiet() |
||||
|
default: |
||||
|
v := in.Option(ctx, "verbose").Bools() |
||||
|
|
||||
|
switch { |
||||
|
case len(v) == verboseInfo: |
||||
|
out = verbosity.Verb(out, output.VerbosityInfo) |
||||
|
case len(v) == verboseDebug: |
||||
|
out = verbosity.Verb(out, output.VerbosityDebug) |
||||
|
case len(v) >= verboseTrace: |
||||
|
out = verbosity.Verb(out, output.VerbosityTrace) |
||||
|
default: |
||||
|
out = verbosity.Verb(out, output.VerbosityNorm) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return out |
||||
|
} |
||||
|
|
||||
|
func showHelp(ctx context.Context, cmd *Command, in input.Input, out output.Output) error { |
||||
|
in.SetArgument(HelpArgumentCommandName, value.New(cmd.Name)) |
||||
|
in.SetOption("help", value.New(false)) |
||||
|
|
||||
|
if _, err := Find(cmd.Name); errors.Is(err, ErrNotFound) { |
||||
|
register(cmd) |
||||
|
} |
||||
|
|
||||
|
help, err := Find(CommandHelp) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
return Run(ctx, help, in, out) |
||||
|
} |
||||
|
|
||||
|
// Default options and argument command.
|
||||
|
func Default(d *input.Definition) *input.Definition { |
||||
|
return d.SetOptions( |
||||
|
option.Bool("no-ansi", "Disable ANSI output"), |
||||
|
option.Bool("ansi", "Do not ask any interactive question"), |
||||
|
option.Bool("version", "Display this application version", option.Short("V")), |
||||
|
option.Bool("help", "Display this help message", option.Short("h")), |
||||
|
option.Bool("verbose", |
||||
|
"Increase the verbosity of messages: -v for info output, -vv for debug and -vvv for trace", |
||||
|
option.Short("v"), option.Array), |
||||
|
option.Bool("quiet", "Do not output any message", option.Short("q")), |
||||
|
) |
||||
|
} |
@ -0,0 +1,45 @@ |
|||||
|
package console_test |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"fmt" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console" |
||||
|
"gitoa.ru/go-4devs/console/input/array" |
||||
|
"gitoa.ru/go-4devs/console/input/value" |
||||
|
"gitoa.ru/go-4devs/console/output/writer" |
||||
|
) |
||||
|
|
||||
|
func ExampleRun() { |
||||
|
cmd := Command() |
||||
|
ctx := context.Background() |
||||
|
out := writer.Stdout() |
||||
|
in := array.New() |
||||
|
|
||||
|
err := console.Run(ctx, cmd, in, out) |
||||
|
fmt.Println("err:", err) |
||||
|
// Output:
|
||||
|
// test argument:
|
||||
|
// bool option:false
|
||||
|
// duration option with default:1s
|
||||
|
// array string:[]
|
||||
|
// err: <nil>
|
||||
|
} |
||||
|
|
||||
|
func ExampleExecute() { |
||||
|
cmd := Command() |
||||
|
ctx := context.Background() |
||||
|
in := array.New() |
||||
|
|
||||
|
// Run command: ./bin "argument value" -b --string="same value" --string="other value"
|
||||
|
in.SetOption("bool", value.New(true)) |
||||
|
in.SetOption("string", value.New([]string{"same value", "other value"})) |
||||
|
in.SetArgument("test_argument", value.New("argument value")) |
||||
|
|
||||
|
console.Execute(ctx, cmd, console.WithInput(in), console.WithExit(func(int) {})) |
||||
|
// Output:
|
||||
|
// test argument:argument value
|
||||
|
// bool option:true
|
||||
|
// duration option with default:1s
|
||||
|
// array string:[same value,other value]
|
||||
|
} |
@ -0,0 +1,89 @@ |
|||||
|
package descriptor |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"errors" |
||||
|
"sync" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/output" |
||||
|
) |
||||
|
|
||||
|
var ErrDescriptorNotFound = errors.New("descriptor not found") |
||||
|
|
||||
|
//nolint: gochecknoglobals
|
||||
|
var ( |
||||
|
descriptors = map[string]Descriptor{ |
||||
|
"txt": &txt{}, |
||||
|
} |
||||
|
descriptorMu sync.Mutex |
||||
|
) |
||||
|
|
||||
|
type Command struct { |
||||
|
Bin string |
||||
|
Name string |
||||
|
Description string |
||||
|
Help string |
||||
|
Definition *input.Definition |
||||
|
} |
||||
|
|
||||
|
type Commands struct { |
||||
|
Namespace string |
||||
|
Definition *input.Definition |
||||
|
Commands []NSCommand |
||||
|
} |
||||
|
|
||||
|
type NSCommand struct { |
||||
|
Name string |
||||
|
Commands []ShortCommand |
||||
|
} |
||||
|
|
||||
|
func (n *NSCommand) Append(name, desc string) { |
||||
|
n.Commands = append(n.Commands, ShortCommand{Name: name, Description: desc}) |
||||
|
} |
||||
|
|
||||
|
type ShortCommand struct { |
||||
|
Name string |
||||
|
Description string |
||||
|
} |
||||
|
|
||||
|
type Descriptor interface { |
||||
|
Command(ctx context.Context, out output.Output, cmd Command) error |
||||
|
Commands(ctx context.Context, out output.Output, cmds Commands) error |
||||
|
} |
||||
|
|
||||
|
func Find(name string) (Descriptor, error) { |
||||
|
descriptorMu.Lock() |
||||
|
defer descriptorMu.Unlock() |
||||
|
|
||||
|
if d, has := descriptors[name]; has { |
||||
|
return d, nil |
||||
|
} |
||||
|
|
||||
|
return nil, ErrDescriptorNotFound |
||||
|
} |
||||
|
|
||||
|
func Descriptors() []string { |
||||
|
names := make([]string, 0, len(descriptors)) |
||||
|
|
||||
|
for name := range descriptors { |
||||
|
names = append(names, name) |
||||
|
} |
||||
|
|
||||
|
return names |
||||
|
} |
||||
|
|
||||
|
func Register(name string, descriptor Descriptor) { |
||||
|
descriptorMu.Lock() |
||||
|
defer descriptorMu.Unlock() |
||||
|
|
||||
|
if descriptor == nil { |
||||
|
panic("console: Register descriptor is nil") |
||||
|
} |
||||
|
|
||||
|
if _, has := descriptors[name]; has { |
||||
|
panic("console: Register called twice for descriptor " + name) |
||||
|
} |
||||
|
|
||||
|
descriptors[name] = descriptor |
||||
|
} |
@ -0,0 +1,371 @@ |
|||||
|
package descriptor |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"context" |
||||
|
"fmt" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
"text/template" |
||||
|
"time" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/output" |
||||
|
) |
||||
|
|
||||
|
const ( |
||||
|
defaultSpace = 2 |
||||
|
infoLen = 13 |
||||
|
) |
||||
|
|
||||
|
//nolint:gochecknoglobals
|
||||
|
var ( |
||||
|
txtFunc = template.FuncMap{ |
||||
|
"synopsis": txtSynopsis, |
||||
|
"definition": txtDefinition, |
||||
|
"help": txtHelp, |
||||
|
"commands": txtCommands, |
||||
|
} |
||||
|
|
||||
|
txtHelpTemplate = template.Must(template.New("txt_template"). |
||||
|
Funcs(txtFunc). |
||||
|
Parse(` |
||||
|
{{- if .Description -}} |
||||
|
<comment>Description:</comment> |
||||
|
{{ .Description }} |
||||
|
|
||||
|
{{ end -}} |
||||
|
<comment>Usage:</comment> |
||||
|
{{ .Name }} {{ synopsis .Definition }} |
||||
|
{{- definition .Definition }} |
||||
|
{{- help . }} |
||||
|
`)) |
||||
|
|
||||
|
txtListTempkate = template.Must(template.New("txt_list"). |
||||
|
Funcs(txtFunc). |
||||
|
Parse(`<comment>Usage:</comment> |
||||
|
command [options] [arguments] |
||||
|
{{- definition .Definition }} |
||||
|
{{- commands .Commands -}} |
||||
|
`)) |
||||
|
) |
||||
|
|
||||
|
type txt struct{} |
||||
|
|
||||
|
func (t *txt) Command(ctx context.Context, out output.Output, cmd Command) error { |
||||
|
var tpl bytes.Buffer |
||||
|
|
||||
|
if err := txtHelpTemplate.Execute(&tpl, cmd); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
out.Println(ctx, tpl.String()) |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (t *txt) Commands(ctx context.Context, out output.Output, cmds Commands) error { |
||||
|
var buf bytes.Buffer |
||||
|
|
||||
|
if err := txtListTempkate.Execute(&buf, cmds); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
out.Println(ctx, buf.String()) |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func txtDefaultArray(val input.Value, flag input.Flag) string { |
||||
|
st := val.Strings() |
||||
|
|
||||
|
switch { |
||||
|
case flag.IsInt(): |
||||
|
for _, i := range val.Ints() { |
||||
|
st = append(st, strconv.Itoa(i)) |
||||
|
} |
||||
|
case flag.IsInt64(): |
||||
|
for _, i := range val.Int64s() { |
||||
|
st = append(st, strconv.FormatInt(i, 10)) |
||||
|
} |
||||
|
case flag.IsUint(): |
||||
|
for _, u := range val.Uints() { |
||||
|
st = append(st, strconv.FormatUint(uint64(u), 10)) |
||||
|
} |
||||
|
case flag.IsUint64(): |
||||
|
for _, u := range val.Uint64s() { |
||||
|
st = append(st, strconv.FormatUint(u, 10)) |
||||
|
} |
||||
|
case flag.IsFloat64(): |
||||
|
for _, f := range val.Float64s() { |
||||
|
st = append(st, strconv.FormatFloat(f, 'g', -1, 64)) |
||||
|
} |
||||
|
case flag.IsDuration(): |
||||
|
for _, d := range val.Durations() { |
||||
|
st = append(st, d.String()) |
||||
|
} |
||||
|
case flag.IsTime(): |
||||
|
for _, d := range val.Times() { |
||||
|
st = append(st, d.Format(time.RFC3339)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return strings.Join(st, ",") |
||||
|
} |
||||
|
|
||||
|
func txtDefault(val input.Value, flag input.Flag) []byte { |
||||
|
var buf bytes.Buffer |
||||
|
|
||||
|
buf.WriteString("<comment> [default: ") |
||||
|
|
||||
|
switch { |
||||
|
case flag.IsArray(): |
||||
|
buf.WriteString(txtDefaultArray(val, flag)) |
||||
|
case flag.IsInt(): |
||||
|
buf.WriteString(strconv.Itoa(val.Int())) |
||||
|
case flag.IsInt64(): |
||||
|
buf.WriteString(strconv.FormatInt(val.Int64(), 10)) |
||||
|
case flag.IsUint(): |
||||
|
buf.WriteString(strconv.FormatUint(uint64(val.Uint()), 10)) |
||||
|
case flag.IsUint64(): |
||||
|
buf.WriteString(strconv.FormatUint(val.Uint64(), 10)) |
||||
|
case flag.IsFloat64(): |
||||
|
buf.WriteString(strconv.FormatFloat(val.Float64(), 'g', -1, 64)) |
||||
|
case flag.IsDuration(): |
||||
|
buf.WriteString(val.Duration().String()) |
||||
|
case flag.IsTime(): |
||||
|
buf.WriteString(val.Time().Format(time.RFC3339)) |
||||
|
case flag.IsAny(): |
||||
|
buf.WriteString(fmt.Sprint(val.Any())) |
||||
|
default: |
||||
|
buf.WriteString(val.String()) |
||||
|
} |
||||
|
|
||||
|
buf.WriteString("]</comment>") |
||||
|
|
||||
|
return buf.Bytes() |
||||
|
} |
||||
|
|
||||
|
func txtCommands(cmds []NSCommand) string { |
||||
|
max := commandsTotalWidth(cmds) |
||||
|
showNS := len(cmds) > 1 |
||||
|
|
||||
|
var buf bytes.Buffer |
||||
|
|
||||
|
buf.WriteString("\n<comment>Available commands") |
||||
|
|
||||
|
if len(cmds) == 1 && cmds[0].Name != "" { |
||||
|
buf.WriteString("for the \"") |
||||
|
buf.WriteString(cmds[0].Name) |
||||
|
buf.WriteString(`" namespace`) |
||||
|
} |
||||
|
|
||||
|
buf.WriteString(":</comment>\n") |
||||
|
|
||||
|
for _, ns := range cmds { |
||||
|
if ns.Name != "" && showNS { |
||||
|
buf.WriteString("<comment>") |
||||
|
buf.WriteString(ns.Name) |
||||
|
buf.WriteString("</comment>\n") |
||||
|
} |
||||
|
|
||||
|
for _, cmd := range ns.Commands { |
||||
|
buf.WriteString(" <info>") |
||||
|
buf.WriteString(cmd.Name) |
||||
|
buf.WriteString("</info>") |
||||
|
buf.WriteString(strings.Repeat(" ", max-len(cmd.Name)+defaultSpace)) |
||||
|
buf.WriteString(cmd.Description) |
||||
|
buf.WriteString("\n") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return buf.String() |
||||
|
} |
||||
|
|
||||
|
func txtHelp(cmd Command) string { |
||||
|
if cmd.Help == "" { |
||||
|
return "" |
||||
|
} |
||||
|
|
||||
|
tpl := template.Must(template.New("help").Parse(cmd.Help)) |
||||
|
|
||||
|
var buf bytes.Buffer |
||||
|
|
||||
|
buf.WriteString("\n<comment>Help:</comment>") |
||||
|
_ = tpl.Execute(&buf, cmd) |
||||
|
|
||||
|
return buf.String() |
||||
|
} |
||||
|
|
||||
|
func txtDefinitionOption(maxLen int, def *input.Definition) string { |
||||
|
buf := bytes.Buffer{} |
||||
|
opts := def.Options() |
||||
|
|
||||
|
buf.WriteString("\n\n<comment>Options:</comment>\n") |
||||
|
|
||||
|
for _, name := range opts { |
||||
|
opt, _ := def.Option(name) |
||||
|
|
||||
|
var op bytes.Buffer |
||||
|
|
||||
|
op.WriteString(" <info>") |
||||
|
|
||||
|
if opt.HasShort() { |
||||
|
op.WriteString("-") |
||||
|
op.WriteString(opt.Short) |
||||
|
op.WriteString(", ") |
||||
|
} else { |
||||
|
op.WriteString(" ") |
||||
|
} |
||||
|
|
||||
|
op.WriteString("--") |
||||
|
op.WriteString(opt.Name) |
||||
|
|
||||
|
if !opt.IsBool() { |
||||
|
if !opt.IsRequired() { |
||||
|
op.WriteString("[") |
||||
|
} |
||||
|
|
||||
|
op.WriteString("=") |
||||
|
op.WriteString(strings.ToUpper(opt.Name)) |
||||
|
|
||||
|
if !opt.IsRequired() { |
||||
|
op.WriteString("]") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
op.WriteString("</info>") |
||||
|
buf.Write(op.Bytes()) |
||||
|
buf.WriteString(strings.Repeat(" ", maxLen+17-op.Len())) |
||||
|
buf.WriteString(opt.Description) |
||||
|
|
||||
|
if opt.HasDefault() { |
||||
|
buf.Write(txtDefault(opt.Default, opt.Flag)) |
||||
|
} |
||||
|
|
||||
|
if opt.IsArray() { |
||||
|
buf.WriteString("<comment> (multiple values allowed)</comment>") |
||||
|
} |
||||
|
|
||||
|
buf.WriteString("\n") |
||||
|
} |
||||
|
|
||||
|
return buf.String() |
||||
|
} |
||||
|
|
||||
|
func txtDefinition(def *input.Definition) string { |
||||
|
max := totalWidth(def) |
||||
|
|
||||
|
var buf bytes.Buffer |
||||
|
|
||||
|
if args := def.Arguments(); len(args) > 0 { |
||||
|
buf.WriteString("\n\n<comment>Arguments:</comment>\n") |
||||
|
|
||||
|
for pos := range args { |
||||
|
var ab bytes.Buffer |
||||
|
|
||||
|
arg, _ := def.Argument(pos) |
||||
|
|
||||
|
ab.WriteString(" <info>") |
||||
|
ab.WriteString(arg.Name) |
||||
|
ab.WriteString("</info>") |
||||
|
ab.WriteString(strings.Repeat(" ", max+infoLen+defaultSpace-ab.Len())) |
||||
|
|
||||
|
buf.Write(ab.Bytes()) |
||||
|
buf.WriteString(arg.Description) |
||||
|
|
||||
|
if arg.HasDefault() { |
||||
|
buf.Write(txtDefault(arg.Default, arg.Flag)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if opts := def.Options(); len(opts) > 0 { |
||||
|
buf.WriteString(txtDefinitionOption(max, def)) |
||||
|
} |
||||
|
|
||||
|
return buf.String() |
||||
|
} |
||||
|
|
||||
|
func txtSynopsis(def *input.Definition) string { |
||||
|
var buf bytes.Buffer |
||||
|
|
||||
|
if len(def.Options()) > 0 { |
||||
|
buf.WriteString("[options] ") |
||||
|
} |
||||
|
|
||||
|
if buf.Len() > 0 && len(def.Arguments()) > 0 { |
||||
|
buf.WriteString("[--]") |
||||
|
} |
||||
|
|
||||
|
var opt int |
||||
|
|
||||
|
for pos := range def.Arguments() { |
||||
|
buf.WriteString(" ") |
||||
|
|
||||
|
arg, _ := def.Argument(pos) |
||||
|
|
||||
|
if !arg.IsRequired() { |
||||
|
buf.WriteString("[") |
||||
|
opt++ |
||||
|
} |
||||
|
|
||||
|
buf.WriteString("<") |
||||
|
buf.WriteString(arg.Name) |
||||
|
buf.WriteString(">") |
||||
|
|
||||
|
if arg.IsArray() { |
||||
|
buf.WriteString("...") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
buf.WriteString(strings.Repeat("]", opt)) |
||||
|
|
||||
|
return buf.String() |
||||
|
} |
||||
|
|
||||
|
func commandsTotalWidth(cmds []NSCommand) int { |
||||
|
var max int |
||||
|
|
||||
|
for _, ns := range cmds { |
||||
|
for _, cmd := range ns.Commands { |
||||
|
if len(cmd.Name) > max { |
||||
|
max = len(cmd.Name) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return max |
||||
|
} |
||||
|
|
||||
|
func totalWidth(def *input.Definition) int { |
||||
|
var max int |
||||
|
|
||||
|
for pos := range def.Arguments() { |
||||
|
arg, _ := def.Argument(pos) |
||||
|
l := len(arg.Name) |
||||
|
|
||||
|
if l > max { |
||||
|
max = l |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for _, name := range def.Options() { |
||||
|
opt, _ := def.Option(name) |
||||
|
l := len(opt.Name) + 6 |
||||
|
|
||||
|
if !opt.IsBool() { |
||||
|
l = l*2 + 1 |
||||
|
} |
||||
|
|
||||
|
if opt.HasDefault() { |
||||
|
l += 2 |
||||
|
} |
||||
|
|
||||
|
if l > max { |
||||
|
max = l |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return max |
||||
|
} |
@ -0,0 +1,30 @@ |
|||||
|
// Package console eases the creation of beautiful and testable command line interfaces.
|
||||
|
// The Console package allows you to create command-line commands.
|
||||
|
// Your console commands can be used for any recurring task, such as cronjobs, imports, or other batch jobs.
|
||||
|
// console application can be written as follows:
|
||||
|
// //cmd/console/main.go
|
||||
|
// func main() {
|
||||
|
// console.New().Execute(context.Background())
|
||||
|
// }
|
||||
|
// Then, you can register the commands using Add():
|
||||
|
// package main
|
||||
|
//
|
||||
|
// import (
|
||||
|
// "context"
|
||||
|
//
|
||||
|
// "gitoa.ru/go-4devs/console"
|
||||
|
// "gitoa.ru/go-4devs/console/example/pkg/command"
|
||||
|
// )
|
||||
|
//
|
||||
|
// func main() {
|
||||
|
// console.
|
||||
|
// New().
|
||||
|
// Add(
|
||||
|
// command.Hello(),
|
||||
|
// command.Args(),
|
||||
|
// command.Hidden(),
|
||||
|
// command.Namespace(),
|
||||
|
// ).
|
||||
|
// Execute(context.Background())
|
||||
|
// }
|
||||
|
package console |
Binary file not shown.
@ -0,0 +1,25 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"os" |
||||
|
"os/signal" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console" |
||||
|
"gitoa.ru/go-4devs/console/example/pkg/command" |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
ctx, cancel := context.WithCancel(context.Background()) |
||||
|
|
||||
|
ch := make(chan os.Signal, 1) |
||||
|
defer close(ch) |
||||
|
|
||||
|
signal.Notify(ch, os.Interrupt) |
||||
|
|
||||
|
go func() { |
||||
|
<-ch |
||||
|
cancel() |
||||
|
}() |
||||
|
console.Execute(ctx, command.Long()) |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console" |
||||
|
"gitoa.ru/go-4devs/console/example/pkg/command" |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
console. |
||||
|
New(). |
||||
|
Add( |
||||
|
command.Hello(), |
||||
|
command.Args(), |
||||
|
command.Hidden(), |
||||
|
command.Namespace(), |
||||
|
command.CreateUser(false), |
||||
|
). |
||||
|
Execute(context.Background()) |
||||
|
} |
@ -0,0 +1,12 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console" |
||||
|
"gitoa.ru/go-4devs/console/example/pkg/command" |
||||
|
) |
||||
|
|
||||
|
func main() { |
||||
|
console.Execute(context.Background(), command.Hello()) |
||||
|
} |
@ -0,0 +1,33 @@ |
|||||
|
package command |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console" |
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/input/option" |
||||
|
"gitoa.ru/go-4devs/console/output" |
||||
|
) |
||||
|
|
||||
|
func Args() *console.Command { |
||||
|
return &console.Command{ |
||||
|
Name: "fdevs:console:arg", |
||||
|
Description: "Understanding how Console Arguments and Options Are Handled", |
||||
|
Configure: func(ctx context.Context, def *input.Definition) error { |
||||
|
def.SetOptions( |
||||
|
option.Bool("foo", "foo option", option.Short("f")), |
||||
|
input.NewOption("bar", "required bar option", option.Required, option.Short("b")), |
||||
|
input.NewOption("cat", "cat option", option.Short("c")), |
||||
|
) |
||||
|
|
||||
|
return nil |
||||
|
}, |
||||
|
Execute: func(ctx context.Context, in input.Input, out output.Output) error { |
||||
|
out.Println(ctx, "foo: <info>", in.Option(ctx, "foo").Bool(), "</info>") |
||||
|
out.Println(ctx, "bar: <info>", in.Option(ctx, "bar").String(), "</info>") |
||||
|
out.Println(ctx, "cat: <info>", in.Option(ctx, "cat").String(), "</info>") |
||||
|
|
||||
|
return nil |
||||
|
}, |
||||
|
} |
||||
|
} |
@ -0,0 +1,36 @@ |
|||||
|
package command |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console" |
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/input/argument" |
||||
|
"gitoa.ru/go-4devs/console/output" |
||||
|
) |
||||
|
|
||||
|
func CreateUser(required bool) *console.Command { |
||||
|
return &console.Command{ |
||||
|
Name: "app:create-user", |
||||
|
Description: "Creates a new user.", |
||||
|
Help: "This command allows you to create a user...", |
||||
|
Configure: func(ctx context.Context, cfg *input.Definition) error { |
||||
|
var opts []func(*input.Argument) |
||||
|
if required { |
||||
|
opts = append(opts, argument.Required) |
||||
|
} |
||||
|
cfg. |
||||
|
SetArgument("username", "The username of the user.", argument.Required). |
||||
|
SetArgument("password", "User password", opts...) |
||||
|
|
||||
|
return nil |
||||
|
}, |
||||
|
Execute: func(ctx context.Context, in input.Input, out output.Output) error { |
||||
|
// outputs a message followed by a "\n"
|
||||
|
out.Println(ctx, "User Creator") |
||||
|
out.Println(ctx, "Username: ", in.Argument(ctx, "username").String()) |
||||
|
|
||||
|
return nil |
||||
|
}, |
||||
|
} |
||||
|
} |
@ -0,0 +1,30 @@ |
|||||
|
package command_test |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"context" |
||||
|
"testing" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console" |
||||
|
"gitoa.ru/go-4devs/console/example/pkg/command" |
||||
|
"gitoa.ru/go-4devs/console/input/array" |
||||
|
"gitoa.ru/go-4devs/console/output/writer" |
||||
|
) |
||||
|
|
||||
|
func TestCreateUser(t *testing.T) { |
||||
|
ctx := context.Background() |
||||
|
in := array.New(array.Argument("username", "andrey")) |
||||
|
buf := bytes.Buffer{} |
||||
|
out := writer.Buffer(&buf) |
||||
|
|
||||
|
err := console.Run(ctx, command.CreateUser(false), in, out) |
||||
|
if err != nil { |
||||
|
t.Fatalf("expect nil err, got :%s", err) |
||||
|
} |
||||
|
|
||||
|
expect := "User Creator\nUsername: andrey\n" |
||||
|
|
||||
|
if expect != buf.String() { |
||||
|
t.Errorf("expect: %s, got:%s", expect, buf.String()) |
||||
|
} |
||||
|
} |
@ -0,0 +1,34 @@ |
|||||
|
package command |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console" |
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/input/argument" |
||||
|
"gitoa.ru/go-4devs/console/output" |
||||
|
) |
||||
|
|
||||
|
func Hello() *console.Command { |
||||
|
return &console.Command{ |
||||
|
Name: "fdevs:console:hello", |
||||
|
Description: "example hello command", |
||||
|
Execute: func(ctx context.Context, in input.Input, out output.Output) error { |
||||
|
name := in.Argument(ctx, "name").String() |
||||
|
out.Println(ctx, "<error>Hello</error> <info>", name, "</info>") |
||||
|
|
||||
|
out.Info(ctx, "same trace info\n") |
||||
|
out.Debug(ctx, "have some question?\n") |
||||
|
out.Trace(ctx, "this message shows with -vvv\n") |
||||
|
|
||||
|
return nil |
||||
|
}, |
||||
|
Configure: func(_ context.Context, def *input.Definition) error { |
||||
|
def.SetArguments( |
||||
|
input.NewArgument("name", "Same name", argument.Default("World")), |
||||
|
) |
||||
|
|
||||
|
return nil |
||||
|
}, |
||||
|
} |
||||
|
} |
@ -0,0 +1,22 @@ |
|||||
|
package command |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console" |
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/output" |
||||
|
) |
||||
|
|
||||
|
func Hidden() *console.Command { |
||||
|
return &console.Command{ |
||||
|
Name: "fdevs:console:hidden", |
||||
|
Description: "hidden command exmale", |
||||
|
Hidden: true, |
||||
|
Execute: func(ctx context.Context, _ input.Input, out output.Output) error { |
||||
|
out.Println(ctx, "<info> call hidden command</info>") |
||||
|
|
||||
|
return nil |
||||
|
}, |
||||
|
} |
||||
|
} |
@ -0,0 +1,50 @@ |
|||||
|
package command |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"time" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console" |
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/input/option" |
||||
|
"gitoa.ru/go-4devs/console/output" |
||||
|
"gitoa.ru/go-4devs/console/validator" |
||||
|
) |
||||
|
|
||||
|
const defaultTimeout = time.Second * 30 |
||||
|
|
||||
|
// Long example of a command that takes a long time to run.
|
||||
|
func Long() *console.Command { |
||||
|
return &console.Command{ |
||||
|
Name: "fdevs:command:long", |
||||
|
Execute: func(ctx context.Context, in input.Input, out output.Output) error { |
||||
|
timeout := in.Option(ctx, "timeout").Duration() |
||||
|
timer := time.NewTimer(timeout) |
||||
|
ticker := time.NewTicker(time.Second) |
||||
|
defer ticker.Stop() |
||||
|
for { |
||||
|
select { |
||||
|
case t := <-ticker.C: |
||||
|
out.Println(ctx, "ticker: <info>", t, "</info>") |
||||
|
case <-timer.C: |
||||
|
out.Println(ctx, "<error>stop timer</error>") |
||||
|
|
||||
|
return nil |
||||
|
case <-ctx.Done(): |
||||
|
out.Println(ctx, "<info>cancel context</info>") |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
Configure: func(ctx context.Context, def *input.Definition) error { |
||||
|
def.SetOptions(option.Duration("timeout", "set duration run command", |
||||
|
option.Default(defaultTimeout), |
||||
|
option.Short("t"), |
||||
|
option.Valid(validator.NotBlank(input.ValueDuration)), |
||||
|
)) |
||||
|
|
||||
|
return nil |
||||
|
}, |
||||
|
} |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
package command |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console" |
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/output" |
||||
|
) |
||||
|
|
||||
|
func Namespace() *console.Command { |
||||
|
return &console.Command{ |
||||
|
Name: "app:start", |
||||
|
Description: "example command in other namespace", |
||||
|
Execute: func(ctx context.Context, _ input.Input, out output.Output) error { |
||||
|
out.Println(ctx, "example command in other namespace") |
||||
|
|
||||
|
return nil |
||||
|
}, |
||||
|
} |
||||
|
} |
@ -0,0 +1,3 @@ |
|||||
|
module gitoa.ru/go-4devs/console |
||||
|
|
||||
|
go 1.15 |
@ -0,0 +1,92 @@ |
|||||
|
package console |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"fmt" |
||||
|
"os" |
||||
|
"strings" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/descriptor" |
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/input/argument" |
||||
|
"gitoa.ru/go-4devs/console/input/option" |
||||
|
"gitoa.ru/go-4devs/console/output" |
||||
|
"gitoa.ru/go-4devs/console/validator" |
||||
|
) |
||||
|
|
||||
|
//nolint: gochecknoinits
|
||||
|
func init() { |
||||
|
MustRegister(help()) |
||||
|
} |
||||
|
|
||||
|
const ( |
||||
|
HelpArgumentCommandName = "command_name" |
||||
|
helpOptFormat = "format" |
||||
|
) |
||||
|
|
||||
|
func help() *Command { |
||||
|
return &Command{ |
||||
|
Name: CommandHelp, |
||||
|
Description: `Displays help for a command`, |
||||
|
Help: ` |
||||
|
The <info>{{ .Name }}</info> command displays help for a given command: |
||||
|
<info>{{ .Bin }} {{ .Name }} list</info> |
||||
|
You can also output the help in other formats by using the <comment>--format</comment> option: |
||||
|
<info>{{ .Bin }} {{ .Name }} --format=xml list</info> |
||||
|
To display the list of available commands, please use the <info>list</info> command. |
||||
|
`, |
||||
|
Execute: func(ctx context.Context, in input.Input, out output.Output) error { |
||||
|
var err error |
||||
|
name := in.Argument(ctx, HelpArgumentCommandName).String() |
||||
|
format := in.Option(ctx, helpOptFormat).String() |
||||
|
|
||||
|
des, err := descriptor.Find(format) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
cmd, err := Find(name) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
def := input.NewDefinition() |
||||
|
|
||||
|
if err := cmd.Init(ctx, Default(def)); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
var bin string |
||||
|
if len(os.Args) > 0 { |
||||
|
bin = os.Args[0] |
||||
|
} |
||||
|
|
||||
|
return des.Command(ctx, out, descriptor.Command{ |
||||
|
Bin: bin, |
||||
|
Name: cmd.Name, |
||||
|
Description: cmd.Description, |
||||
|
Help: cmd.Help, |
||||
|
Definition: def, |
||||
|
}) |
||||
|
}, |
||||
|
Configure: func(ctx context.Context, config *input.Definition) error { |
||||
|
formats := descriptor.Descriptors() |
||||
|
config. |
||||
|
SetArguments( |
||||
|
input.NewArgument(HelpArgumentCommandName, "The command name", argument.Default("help")), |
||||
|
). |
||||
|
SetOptions( |
||||
|
input.NewOption(helpOptFormat, fmt.Sprintf("The output format (%s)", strings.Join(formats, ", ")), |
||||
|
option.Required, |
||||
|
option.Default(formats[0]), |
||||
|
option.Valid( |
||||
|
validator.NotBlank(input.ValueString), |
||||
|
validator.Enum(formats...), |
||||
|
), |
||||
|
), |
||||
|
) |
||||
|
|
||||
|
return nil |
||||
|
}, |
||||
|
} |
||||
|
} |
@ -0,0 +1,48 @@ |
|||||
|
package input |
||||
|
|
||||
|
func NewArgument(name, description string, opts ...func(*Argument)) Argument { |
||||
|
a := Argument{ |
||||
|
Name: name, |
||||
|
Description: description, |
||||
|
} |
||||
|
|
||||
|
for _, opt := range opts { |
||||
|
opt(&a) |
||||
|
} |
||||
|
|
||||
|
return a |
||||
|
} |
||||
|
|
||||
|
type Argument struct { |
||||
|
Name string |
||||
|
Description string |
||||
|
Default Value |
||||
|
Flag Flag |
||||
|
Valid []func(Value) error |
||||
|
} |
||||
|
|
||||
|
func (a Argument) HasDefault() bool { |
||||
|
return a.Default != nil |
||||
|
} |
||||
|
|
||||
|
func (a Argument) IsBool() bool { |
||||
|
return a.Flag.IsBool() |
||||
|
} |
||||
|
|
||||
|
func (a Argument) IsRequired() bool { |
||||
|
return a.Flag.IsRequired() |
||||
|
} |
||||
|
|
||||
|
func (a Argument) IsArray() bool { |
||||
|
return a.Flag.IsArray() |
||||
|
} |
||||
|
|
||||
|
func (a Argument) Validate(v Value) error { |
||||
|
for _, valid := range a.Valid { |
||||
|
if err := valid(v); err != nil { |
||||
|
return ErrorArgument(a.Name, err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
@ -0,0 +1,26 @@ |
|||||
|
package argument |
||||
|
|
||||
|
import ( |
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/input/value" |
||||
|
) |
||||
|
|
||||
|
func Required(a *input.Argument) { |
||||
|
a.Flag |= input.ValueRequired |
||||
|
} |
||||
|
|
||||
|
func Default(v interface{}) func(*input.Argument) { |
||||
|
return func(a *input.Argument) { |
||||
|
a.Default = value.New(v) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func Flag(flag input.Flag) func(*input.Argument) { |
||||
|
return func(a *input.Argument) { |
||||
|
a.Flag = flag |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func Array(a *input.Argument) { |
||||
|
a.Flag |= input.ValueArray |
||||
|
} |
@ -0,0 +1,211 @@ |
|||||
|
package argv |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"fmt" |
||||
|
"strings" |
||||
|
"sync" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/input/value" |
||||
|
"gitoa.ru/go-4devs/console/input/wrap" |
||||
|
) |
||||
|
|
||||
|
const doubleDash = `--` |
||||
|
|
||||
|
var _ input.ReadInput = (*Input)(nil) |
||||
|
|
||||
|
func WithErrorHandle(h func(error) error) func(*Input) { |
||||
|
return func(i *Input) { |
||||
|
i.errorHandle = h |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func New(args []string, opts ...func(*Input)) *wrap.Input { |
||||
|
i := &Input{ |
||||
|
args: args, |
||||
|
arguments: make(map[string]input.AppendValue), |
||||
|
options: make(map[string]input.AppendValue), |
||||
|
errorHandle: func(err error) error { |
||||
|
return err |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
for _, opt := range opts { |
||||
|
opt(i) |
||||
|
} |
||||
|
|
||||
|
return &wrap.Input{ReadInput: i} |
||||
|
} |
||||
|
|
||||
|
type Input struct { |
||||
|
args []string |
||||
|
arguments map[string]input.AppendValue |
||||
|
options map[string]input.AppendValue |
||||
|
mu sync.RWMutex |
||||
|
errorHandle func(error) error |
||||
|
} |
||||
|
|
||||
|
func (i *Input) ReadOption(ctx context.Context, name string) (input.Value, error) { |
||||
|
if v, ok := i.options[name]; ok { |
||||
|
return v, nil |
||||
|
} |
||||
|
|
||||
|
return nil, input.ErrNotFound |
||||
|
} |
||||
|
|
||||
|
func (i *Input) SetOption(name string, val input.Value) { |
||||
|
i.mu.Lock() |
||||
|
defer i.mu.Unlock() |
||||
|
|
||||
|
i.options[name] = &value.Read{Value: val} |
||||
|
} |
||||
|
|
||||
|
func (i *Input) ReadArgument(ctx context.Context, name string) (input.Value, error) { |
||||
|
if v, ok := i.arguments[name]; ok { |
||||
|
return v, nil |
||||
|
} |
||||
|
|
||||
|
return nil, input.ErrNotFound |
||||
|
} |
||||
|
|
||||
|
func (i *Input) SetArgument(name string, val input.Value) { |
||||
|
i.mu.Lock() |
||||
|
defer i.mu.Unlock() |
||||
|
|
||||
|
i.arguments[name] = &value.Read{Value: val} |
||||
|
} |
||||
|
|
||||
|
func (i *Input) Bind(ctx context.Context, def *input.Definition) error { |
||||
|
options := true |
||||
|
|
||||
|
for len(i.args) > 0 { |
||||
|
var err error |
||||
|
|
||||
|
arg := i.args[0] |
||||
|
i.args = i.args[1:] |
||||
|
|
||||
|
switch { |
||||
|
case options && arg == doubleDash: |
||||
|
options = false |
||||
|
case options && len(arg) > 2 && arg[0:2] == doubleDash: |
||||
|
err = i.parseLongOption(arg[2:], def) |
||||
|
case options && arg[0:1] == "-": |
||||
|
if len(arg) == 1 { |
||||
|
return fmt.Errorf("%w: option name required given '-'", input.ErrInvalidName) |
||||
|
} |
||||
|
|
||||
|
err = i.parseShortOption(arg[1:], def) |
||||
|
default: |
||||
|
err = i.parseArgument(arg, def) |
||||
|
} |
||||
|
|
||||
|
if err != nil { |
||||
|
if herr := i.errorHandle(err); herr != nil { |
||||
|
return herr |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (i *Input) parseLongOption(arg string, def *input.Definition) error { |
||||
|
var value *string |
||||
|
|
||||
|
name := arg |
||||
|
|
||||
|
if strings.Contains(arg, "=") { |
||||
|
vals := strings.SplitN(arg, "=", 2) |
||||
|
name = vals[0] |
||||
|
value = &vals[1] |
||||
|
} |
||||
|
|
||||
|
opt, err := def.Option(name) |
||||
|
if err != nil { |
||||
|
return input.ErrorOption(name, err) |
||||
|
} |
||||
|
|
||||
|
return i.appendOption(name, value, opt) |
||||
|
} |
||||
|
|
||||
|
func (i *Input) appendOption(name string, data *string, opt input.Option) error { |
||||
|
v, ok := i.options[name] |
||||
|
|
||||
|
if ok && !opt.IsArray() { |
||||
|
return fmt.Errorf("%w: got: array, expect: %s", input.ErrUnexpectedType, input.Type(opt.Flag)) |
||||
|
} |
||||
|
|
||||
|
var val string |
||||
|
|
||||
|
switch { |
||||
|
case data != nil: |
||||
|
val = *data |
||||
|
case opt.IsBool(): |
||||
|
val = "true" |
||||
|
case len(i.args) > 0 && len(i.args[0]) > 0 && i.args[0][0:1] != "-": |
||||
|
val = i.args[0] |
||||
|
i.args = i.args[1:] |
||||
|
default: |
||||
|
return input.ErrorOption(name, input.ErrRequired) |
||||
|
} |
||||
|
|
||||
|
if !ok { |
||||
|
v = value.ByFlag(opt.Flag) |
||||
|
i.options[name] = v |
||||
|
} |
||||
|
|
||||
|
if err := v.Append(val); err != nil { |
||||
|
return input.ErrorOption(name, err) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (i *Input) parseShortOption(arg string, def *input.Definition) error { |
||||
|
name := arg |
||||
|
|
||||
|
var value string |
||||
|
|
||||
|
if len(name) > 1 { |
||||
|
name, value = arg[0:1], arg[1:] |
||||
|
} |
||||
|
|
||||
|
opt, err := def.ShortOption(name) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
if opt.IsBool() && value != "" { |
||||
|
if err := i.parseShortOption(value, def); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
value = "" |
||||
|
} |
||||
|
|
||||
|
if value == "" { |
||||
|
return i.appendOption(opt.Name, nil, opt) |
||||
|
} |
||||
|
|
||||
|
return i.appendOption(opt.Name, &value, opt) |
||||
|
} |
||||
|
|
||||
|
func (i *Input) parseArgument(arg string, def *input.Definition) error { |
||||
|
opt, err := def.Argument(len(i.arguments)) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
v, ok := i.arguments[opt.Name] |
||||
|
if !ok { |
||||
|
v = value.ByFlag(opt.Flag) |
||||
|
i.arguments[opt.Name] = v |
||||
|
} |
||||
|
|
||||
|
if err := v.Append(arg); err != nil { |
||||
|
return input.ErrorArgument(opt.Name, err) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
@ -0,0 +1,87 @@ |
|||||
|
package array |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"sync" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/input/value" |
||||
|
"gitoa.ru/go-4devs/console/input/wrap" |
||||
|
) |
||||
|
|
||||
|
var _ input.ReadInput = (*Input)(nil) |
||||
|
|
||||
|
func Argument(name string, v interface{}) func(*Input) { |
||||
|
return func(i *Input) { |
||||
|
i.args[name] = value.New(v) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func Option(name string, v interface{}) func(*Input) { |
||||
|
return func(i *Input) { |
||||
|
i.opt[name] = value.New(v) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func New(opts ...func(*Input)) *wrap.Input { |
||||
|
i := &Input{ |
||||
|
args: make(map[string]input.Value), |
||||
|
opt: make(map[string]input.Value), |
||||
|
} |
||||
|
|
||||
|
for _, opt := range opts { |
||||
|
opt(i) |
||||
|
} |
||||
|
|
||||
|
return &wrap.Input{ReadInput: i} |
||||
|
} |
||||
|
|
||||
|
type Input struct { |
||||
|
args map[string]input.Value |
||||
|
opt map[string]input.Value |
||||
|
mu sync.Mutex |
||||
|
} |
||||
|
|
||||
|
func (i *Input) ReadOption(_ context.Context, name string) (input.Value, error) { |
||||
|
if o, has := i.opt[name]; has { |
||||
|
return o, nil |
||||
|
} |
||||
|
|
||||
|
return nil, input.ErrorOption(name, input.ErrNotFound) |
||||
|
} |
||||
|
|
||||
|
func (i *Input) HasOption(name string) bool { |
||||
|
_, has := i.opt[name] |
||||
|
|
||||
|
return has |
||||
|
} |
||||
|
|
||||
|
func (i *Input) SetOption(name string, val input.Value) { |
||||
|
i.mu.Lock() |
||||
|
i.opt[name] = val |
||||
|
i.mu.Unlock() |
||||
|
} |
||||
|
|
||||
|
func (i *Input) ReadArgument(_ context.Context, name string) (input.Value, error) { |
||||
|
if a, has := i.args[name]; has { |
||||
|
return a, nil |
||||
|
} |
||||
|
|
||||
|
return nil, input.ErrorArgument(name, input.ErrNotFound) |
||||
|
} |
||||
|
|
||||
|
func (i *Input) HasArgument(name string) bool { |
||||
|
_, has := i.args[name] |
||||
|
|
||||
|
return has |
||||
|
} |
||||
|
|
||||
|
func (i *Input) SetArgument(name string, val input.Value) { |
||||
|
i.mu.Lock() |
||||
|
i.args[name] = val |
||||
|
i.mu.Unlock() |
||||
|
} |
||||
|
|
||||
|
func (i *Input) Bind(_ context.Context, def *input.Definition) error { |
||||
|
return nil |
||||
|
} |
@ -0,0 +1,95 @@ |
|||||
|
package input |
||||
|
|
||||
|
func NewDefinition() *Definition { |
||||
|
return &Definition{ |
||||
|
options: make(map[string]Option), |
||||
|
args: make(map[string]Argument), |
||||
|
short: make(map[string]string), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
type Definition struct { |
||||
|
options map[string]Option |
||||
|
posOpt []string |
||||
|
args map[string]Argument |
||||
|
posArgs []string |
||||
|
short map[string]string |
||||
|
} |
||||
|
|
||||
|
func (d *Definition) Options() []string { |
||||
|
return d.posOpt |
||||
|
} |
||||
|
|
||||
|
func (d *Definition) Arguments() []string { |
||||
|
return d.posArgs |
||||
|
} |
||||
|
|
||||
|
func (d *Definition) SetOption(name, description string, opts ...func(*Option)) *Definition { |
||||
|
return d.SetOptions(NewOption(name, description, opts...)) |
||||
|
} |
||||
|
|
||||
|
func (d *Definition) SetOptions(opts ...Option) *Definition { |
||||
|
for _, opt := range opts { |
||||
|
if _, has := d.options[opt.Name]; !has { |
||||
|
d.posOpt = append([]string{opt.Name}, d.posOpt...) |
||||
|
} |
||||
|
|
||||
|
d.options[opt.Name] = opt |
||||
|
if opt.HasShort() { |
||||
|
d.short[opt.Short] = opt.Name |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return d |
||||
|
} |
||||
|
|
||||
|
func (d *Definition) SetArgument(name, description string, opts ...func(*Argument)) *Definition { |
||||
|
return d.SetArguments(NewArgument(name, description, opts...)) |
||||
|
} |
||||
|
|
||||
|
func (d *Definition) SetArguments(args ...Argument) *Definition { |
||||
|
for _, arg := range args { |
||||
|
if _, ok := d.args[arg.Name]; !ok { |
||||
|
d.posArgs = append(d.posArgs, arg.Name) |
||||
|
} |
||||
|
|
||||
|
d.args[arg.Name] = arg |
||||
|
} |
||||
|
|
||||
|
return d |
||||
|
} |
||||
|
|
||||
|
func (d *Definition) Argument(pos int) (Argument, error) { |
||||
|
if len(d.posArgs) == 0 { |
||||
|
return Argument{}, ErrNoArgs |
||||
|
} |
||||
|
|
||||
|
lastPos := len(d.posArgs) - 1 |
||||
|
if lastPos < pos { |
||||
|
arg := d.args[d.posArgs[lastPos]] |
||||
|
if arg.IsArray() { |
||||
|
return arg, nil |
||||
|
} |
||||
|
|
||||
|
return Argument{}, ErrToManyArgs |
||||
|
} |
||||
|
|
||||
|
return d.args[d.posArgs[pos]], nil |
||||
|
} |
||||
|
|
||||
|
func (d *Definition) ShortOption(short string) (Option, error) { |
||||
|
name, ok := d.short[short] |
||||
|
if !ok { |
||||
|
return Option{}, ErrNotFound |
||||
|
} |
||||
|
|
||||
|
return d.Option(name) |
||||
|
} |
||||
|
|
||||
|
func (d *Definition) Option(name string) (Option, error) { |
||||
|
if opt, ok := d.options[name]; ok { |
||||
|
return opt, nil |
||||
|
} |
||||
|
|
||||
|
return Option{}, ErrNotFound |
||||
|
} |
@ -0,0 +1,50 @@ |
|||||
|
package input |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
) |
||||
|
|
||||
|
var ( |
||||
|
ErrNotFound = errors.New("not found") |
||||
|
ErrNoArgs = errors.New("no arguments expected") |
||||
|
ErrToManyArgs = errors.New("too many arguments") |
||||
|
ErrUnexpectedType = errors.New("unexpected type") |
||||
|
ErrRequired = errors.New("is required") |
||||
|
ErrAppend = errors.New("failed append") |
||||
|
ErrInvalidName = errors.New("invalid name") |
||||
|
) |
||||
|
|
||||
|
type Error struct { |
||||
|
name string |
||||
|
err error |
||||
|
t string |
||||
|
} |
||||
|
|
||||
|
func (o Error) Error() string { |
||||
|
return fmt.Sprintf("%s: '%s' %s", o.t, o.name, o.err) |
||||
|
} |
||||
|
|
||||
|
func (o Error) Is(err error) bool { |
||||
|
return errors.Is(err, o.err) |
||||
|
} |
||||
|
|
||||
|
func (o Error) Unwrap() error { |
||||
|
return o.err |
||||
|
} |
||||
|
|
||||
|
func ErrorOption(name string, err error) Error { |
||||
|
return Error{ |
||||
|
name: name, |
||||
|
err: err, |
||||
|
t: "option", |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func ErrorArgument(name string, err error) Error { |
||||
|
return Error{ |
||||
|
name: name, |
||||
|
err: err, |
||||
|
t: "argument", |
||||
|
} |
||||
|
} |
@ -0,0 +1,76 @@ |
|||||
|
package input |
||||
|
|
||||
|
//go:generate stringer -type=Flag -linecomment
|
||||
|
|
||||
|
type Flag int |
||||
|
|
||||
|
const ( |
||||
|
ValueString Flag = 0 // string
|
||||
|
ValueRequired Flag = 1 << iota // required
|
||||
|
ValueArray // array
|
||||
|
ValueInt // int
|
||||
|
ValueInt64 // int64
|
||||
|
ValueUint // uint
|
||||
|
ValueUint64 // uint64
|
||||
|
ValueFloat64 // float64
|
||||
|
ValueBool // bool
|
||||
|
ValueDuration // duration
|
||||
|
ValueTime // time
|
||||
|
ValueAny // any
|
||||
|
) |
||||
|
|
||||
|
func (f Flag) Type() Flag { |
||||
|
return Type(f) |
||||
|
} |
||||
|
|
||||
|
func (f Flag) With(v Flag) Flag { |
||||
|
return f | v |
||||
|
} |
||||
|
|
||||
|
func (f Flag) IsString() bool { |
||||
|
return f|ValueRequired|ValueArray^ValueRequired^ValueArray == 0 |
||||
|
} |
||||
|
|
||||
|
func (f Flag) IsRequired() bool { |
||||
|
return f&ValueRequired > 0 |
||||
|
} |
||||
|
|
||||
|
func (f Flag) IsArray() bool { |
||||
|
return f&ValueArray > 0 |
||||
|
} |
||||
|
|
||||
|
func (f Flag) IsInt() bool { |
||||
|
return f&ValueInt > 0 |
||||
|
} |
||||
|
|
||||
|
func (f Flag) IsInt64() bool { |
||||
|
return f&ValueInt64 > 0 |
||||
|
} |
||||
|
|
||||
|
func (f Flag) IsUint() bool { |
||||
|
return f&ValueUint > 0 |
||||
|
} |
||||
|
|
||||
|
func (f Flag) IsUint64() bool { |
||||
|
return f&ValueUint64 > 0 |
||||
|
} |
||||
|
|
||||
|
func (f Flag) IsFloat64() bool { |
||||
|
return f&ValueFloat64 > 0 |
||||
|
} |
||||
|
|
||||
|
func (f Flag) IsBool() bool { |
||||
|
return f&ValueBool > 0 |
||||
|
} |
||||
|
|
||||
|
func (f Flag) IsDuration() bool { |
||||
|
return f&ValueDuration > 0 |
||||
|
} |
||||
|
|
||||
|
func (f Flag) IsTime() bool { |
||||
|
return f&ValueTime > 0 |
||||
|
} |
||||
|
|
||||
|
func (f Flag) IsAny() bool { |
||||
|
return f&ValueAny > 0 |
||||
|
} |
@ -0,0 +1,47 @@ |
|||||
|
// Code generated by "stringer -type=Flag -linecomment"; DO NOT EDIT.
|
||||
|
|
||||
|
package input |
||||
|
|
||||
|
import "strconv" |
||||
|
|
||||
|
func _() { |
||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
|
// Re-run the stringer command to generate them again.
|
||||
|
var x [1]struct{} |
||||
|
_ = x[ValueString-0] |
||||
|
_ = x[ValueRequired-2] |
||||
|
_ = x[ValueArray-4] |
||||
|
_ = x[ValueInt-8] |
||||
|
_ = x[ValueInt64-16] |
||||
|
_ = x[ValueUint-32] |
||||
|
_ = x[ValueUint64-64] |
||||
|
_ = x[ValueFloat64-128] |
||||
|
_ = x[ValueBool-256] |
||||
|
_ = x[ValueDuration-512] |
||||
|
_ = x[ValueTime-1024] |
||||
|
_ = x[ValueAny-2048] |
||||
|
} |
||||
|
|
||||
|
const _Flag_name = "stringrequiredarrayintint64uintuint64float64booldurationtimeany" |
||||
|
|
||||
|
var _Flag_map = map[Flag]string{ |
||||
|
0: _Flag_name[0:6], |
||||
|
2: _Flag_name[6:14], |
||||
|
4: _Flag_name[14:19], |
||||
|
8: _Flag_name[19:22], |
||||
|
16: _Flag_name[22:27], |
||||
|
32: _Flag_name[27:31], |
||||
|
64: _Flag_name[31:37], |
||||
|
128: _Flag_name[37:44], |
||||
|
256: _Flag_name[44:48], |
||||
|
512: _Flag_name[48:56], |
||||
|
1024: _Flag_name[56:60], |
||||
|
2048: _Flag_name[60:63], |
||||
|
} |
||||
|
|
||||
|
func (i Flag) String() string { |
||||
|
if str, ok := _Flag_map[i]; ok { |
||||
|
return str |
||||
|
} |
||||
|
return "Flag(" + strconv.FormatInt(int64(i), 10) + ")" |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
package input |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
) |
||||
|
|
||||
|
type ReadInput interface { |
||||
|
Bind(ctx context.Context, def *Definition) error |
||||
|
|
||||
|
ReadOption(ctx context.Context, name string) (Value, error) |
||||
|
SetOption(name string, value Value) |
||||
|
|
||||
|
ReadArgument(ctx context.Context, name string) (Value, error) |
||||
|
SetArgument(name string, value Value) |
||||
|
} |
||||
|
|
||||
|
type Input interface { |
||||
|
Option(ctx context.Context, name string) Value |
||||
|
Argument(ctx context.Context, name string) Value |
||||
|
ReadInput |
||||
|
} |
@ -0,0 +1,53 @@ |
|||||
|
package input |
||||
|
|
||||
|
func NewOption(name, description string, opts ...func(*Option)) Option { |
||||
|
o := Option{ |
||||
|
Name: name, |
||||
|
Description: description, |
||||
|
} |
||||
|
|
||||
|
for _, opt := range opts { |
||||
|
opt(&o) |
||||
|
} |
||||
|
|
||||
|
return o |
||||
|
} |
||||
|
|
||||
|
type Option struct { |
||||
|
Name string |
||||
|
Description string |
||||
|
Short string |
||||
|
Flag Flag |
||||
|
Default Value |
||||
|
Valid []func(Value) error |
||||
|
} |
||||
|
|
||||
|
func (o Option) HasShort() bool { |
||||
|
return len(o.Short) == 1 |
||||
|
} |
||||
|
|
||||
|
func (o Option) HasDefault() bool { |
||||
|
return o.Default != nil |
||||
|
} |
||||
|
|
||||
|
func (o Option) IsBool() bool { |
||||
|
return o.Flag.IsBool() |
||||
|
} |
||||
|
|
||||
|
func (o Option) IsArray() bool { |
||||
|
return o.Flag.IsArray() |
||||
|
} |
||||
|
|
||||
|
func (o Option) IsRequired() bool { |
||||
|
return o.Flag.IsRequired() |
||||
|
} |
||||
|
|
||||
|
func (o Option) Validate(v Value) error { |
||||
|
for _, valid := range o.Valid { |
||||
|
if err := valid(v); err != nil { |
||||
|
return ErrorOption(o.Name, err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
@ -0,0 +1,35 @@ |
|||||
|
package option |
||||
|
|
||||
|
import "gitoa.ru/go-4devs/console/input" |
||||
|
|
||||
|
func Bool(name, description string, opts ...func(*input.Option)) input.Option { |
||||
|
return input.NewOption(name, description, append(opts, Value(input.ValueBool))...) |
||||
|
} |
||||
|
|
||||
|
func Duration(name, description string, opts ...func(*input.Option)) input.Option { |
||||
|
return input.NewOption(name, description, append(opts, Value(input.ValueDuration))...) |
||||
|
} |
||||
|
|
||||
|
func Float64(name, description string, opts ...func(*input.Option)) input.Option { |
||||
|
return input.NewOption(name, description, append(opts, Value(input.ValueFloat64))...) |
||||
|
} |
||||
|
|
||||
|
func Int(name, description string, opts ...func(*input.Option)) input.Option { |
||||
|
return input.NewOption(name, description, append(opts, Value(input.ValueInt))...) |
||||
|
} |
||||
|
|
||||
|
func Int64(name, description string, opts ...func(*input.Option)) input.Option { |
||||
|
return input.NewOption(name, description, append(opts, Value(input.ValueInt64))...) |
||||
|
} |
||||
|
|
||||
|
func Time(name, description string, opts ...func(*input.Option)) input.Option { |
||||
|
return input.NewOption(name, description, append(opts, Value(input.ValueTime))...) |
||||
|
} |
||||
|
|
||||
|
func Uint(name, description string, opts ...func(*input.Option)) input.Option { |
||||
|
return input.NewOption(name, description, append(opts, Value(input.ValueUint))...) |
||||
|
} |
||||
|
|
||||
|
func Uint64(name, descriontion string, opts ...func(*input.Option)) input.Option { |
||||
|
return input.NewOption(name, descriontion, append(opts, Value(input.ValueUint64))...) |
||||
|
} |
@ -0,0 +1,44 @@ |
|||||
|
package option |
||||
|
|
||||
|
import ( |
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/input/value" |
||||
|
) |
||||
|
|
||||
|
func Required(o *input.Option) { |
||||
|
o.Flag |= input.ValueRequired |
||||
|
} |
||||
|
|
||||
|
func Default(in interface{}) func(*input.Option) { |
||||
|
return func(o *input.Option) { |
||||
|
o.Default = value.New(in) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func Short(s string) func(*input.Option) { |
||||
|
return func(o *input.Option) { |
||||
|
o.Short = s |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func Array(o *input.Option) { |
||||
|
o.Flag |= input.ValueArray |
||||
|
} |
||||
|
|
||||
|
func Value(flag input.Flag) func(*input.Option) { |
||||
|
return func(o *input.Option) { |
||||
|
o.Flag |= flag |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func Flag(in input.Flag) func(*input.Option) { |
||||
|
return func(o *input.Option) { |
||||
|
o.Flag = in |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func Valid(f ...func(input.Value) error) func(*input.Option) { |
||||
|
return func(o *input.Option) { |
||||
|
o.Valid = f |
||||
|
} |
||||
|
} |
@ -0,0 +1,58 @@ |
|||||
|
package input |
||||
|
|
||||
|
import ( |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
type Value interface { |
||||
|
String() string |
||||
|
Int() int |
||||
|
Int64() int64 |
||||
|
Uint() uint |
||||
|
Uint64() uint64 |
||||
|
Float64() float64 |
||||
|
Bool() bool |
||||
|
Duration() time.Duration |
||||
|
Time() time.Time |
||||
|
Any() interface{} |
||||
|
|
||||
|
Strings() []string |
||||
|
Ints() []int |
||||
|
Int64s() []int64 |
||||
|
Uints() []uint |
||||
|
Uint64s() []uint64 |
||||
|
Float64s() []float64 |
||||
|
Bools() []bool |
||||
|
Durations() []time.Duration |
||||
|
Times() []time.Time |
||||
|
} |
||||
|
|
||||
|
type AppendValue interface { |
||||
|
Value |
||||
|
Append(string) error |
||||
|
} |
||||
|
|
||||
|
func Type(flag Flag) Flag { |
||||
|
switch { |
||||
|
case (flag & ValueInt) > 0: |
||||
|
return ValueInt |
||||
|
case (flag & ValueInt64) > 0: |
||||
|
return ValueInt64 |
||||
|
case (flag & ValueUint) > 0: |
||||
|
return ValueUint |
||||
|
case (flag & ValueUint64) > 0: |
||||
|
return ValueUint64 |
||||
|
case (flag & ValueFloat64) > 0: |
||||
|
return ValueFloat64 |
||||
|
case (flag & ValueBool) > 0: |
||||
|
return ValueBool |
||||
|
case (flag & ValueDuration) > 0: |
||||
|
return ValueDuration |
||||
|
case (flag & ValueTime) > 0: |
||||
|
return ValueTime |
||||
|
case (flag & ValueAny) > 0: |
||||
|
return ValueAny |
||||
|
default: |
||||
|
return ValueString |
||||
|
} |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
package value |
||||
|
|
||||
|
import "gitoa.ru/go-4devs/console/input" |
||||
|
|
||||
|
type Any struct { |
||||
|
Empty |
||||
|
Val []interface{} |
||||
|
Flag input.Flag |
||||
|
} |
||||
|
|
||||
|
func (a *Any) Any() interface{} { |
||||
|
if a.Flag.IsArray() { |
||||
|
return a.Val |
||||
|
} |
||||
|
|
||||
|
if len(a.Val) > 0 { |
||||
|
return a.Val[0] |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
@ -0,0 +1,44 @@ |
|||||
|
package value |
||||
|
|
||||
|
import ( |
||||
|
"strconv" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
) |
||||
|
|
||||
|
type Bool struct { |
||||
|
Empty |
||||
|
Val []bool |
||||
|
Flag input.Flag |
||||
|
} |
||||
|
|
||||
|
func (b *Bool) Append(in string) error { |
||||
|
v, err := strconv.ParseBool(in) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
b.Val = append(b.Val, v) |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (b *Bool) Bool() bool { |
||||
|
if !b.Flag.IsArray() && len(b.Val) == 1 { |
||||
|
return b.Val[0] |
||||
|
} |
||||
|
|
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func (b *Bool) Bools() []bool { |
||||
|
return b.Val |
||||
|
} |
||||
|
|
||||
|
func (b *Bool) Any() interface{} { |
||||
|
if b.Flag&input.ValueArray > 0 { |
||||
|
return b.Bools() |
||||
|
} |
||||
|
|
||||
|
return b.Bool() |
||||
|
} |
@ -0,0 +1,44 @@ |
|||||
|
package value |
||||
|
|
||||
|
import ( |
||||
|
"time" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
) |
||||
|
|
||||
|
type Duration struct { |
||||
|
Empty |
||||
|
Val []time.Duration |
||||
|
Flag input.Flag |
||||
|
} |
||||
|
|
||||
|
func (d *Duration) Append(in string) error { |
||||
|
v, err := time.ParseDuration(in) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
d.Val = append(d.Val, v) |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (d *Duration) Duration() time.Duration { |
||||
|
if !d.Flag.IsArray() && len(d.Val) == 1 { |
||||
|
return d.Val[0] |
||||
|
} |
||||
|
|
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
func (d *Duration) Durations() []time.Duration { |
||||
|
return d.Val |
||||
|
} |
||||
|
|
||||
|
func (d *Duration) Any() interface{} { |
||||
|
if d.Flag&input.ValueArray > 0 { |
||||
|
return d.Durations() |
||||
|
} |
||||
|
|
||||
|
return d.Duration() |
||||
|
} |
@ -0,0 +1,90 @@ |
|||||
|
package value |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"time" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
) |
||||
|
|
||||
|
type Empty struct{} |
||||
|
|
||||
|
func (e *Empty) Append(string) error { |
||||
|
return fmt.Errorf("%w: in empty value", input.ErrInvalidName) |
||||
|
} |
||||
|
|
||||
|
func (e *Empty) String() string { |
||||
|
return "" |
||||
|
} |
||||
|
|
||||
|
func (e *Empty) Int() int { |
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
func (e *Empty) Int64() int64 { |
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
func (e *Empty) Uint() uint { |
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
func (e *Empty) Uint64() uint64 { |
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
func (e *Empty) Float64() float64 { |
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
func (e *Empty) Bool() bool { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func (e *Empty) Duration() time.Duration { |
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
func (e *Empty) Time() time.Time { |
||||
|
return time.Time{} |
||||
|
} |
||||
|
|
||||
|
func (e *Empty) Strings() []string { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (e *Empty) Ints() []int { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (e *Empty) Int64s() []int64 { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (e *Empty) Uints() []uint { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (e *Empty) Uint64s() []uint64 { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (e *Empty) Float64s() []float64 { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (e *Empty) Bools() []bool { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (e *Empty) Durations() []time.Duration { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (e *Empty) Times() []time.Time { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (e *Empty) Any() interface{} { |
||||
|
return nil |
||||
|
} |
@ -0,0 +1,44 @@ |
|||||
|
package value |
||||
|
|
||||
|
import ( |
||||
|
"strconv" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
) |
||||
|
|
||||
|
type Float64 struct { |
||||
|
Empty |
||||
|
Val []float64 |
||||
|
Flag input.Flag |
||||
|
} |
||||
|
|
||||
|
func (f *Float64) Append(in string) error { |
||||
|
v, err := strconv.ParseFloat(in, 64) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
f.Val = append(f.Val, v) |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (f *Float64) Float64() float64 { |
||||
|
if !f.Flag.IsArray() && len(f.Val) == 1 { |
||||
|
return f.Val[0] |
||||
|
} |
||||
|
|
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
func (f *Float64) Float64s() []float64 { |
||||
|
return f.Val |
||||
|
} |
||||
|
|
||||
|
func (f *Float64) Any() interface{} { |
||||
|
if f.Flag&input.ValueFloat64 > 0 { |
||||
|
return f.Float64s() |
||||
|
} |
||||
|
|
||||
|
return f.Float64() |
||||
|
} |
@ -0,0 +1,44 @@ |
|||||
|
package value |
||||
|
|
||||
|
import ( |
||||
|
"strconv" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
) |
||||
|
|
||||
|
type Int struct { |
||||
|
Empty |
||||
|
Val []int |
||||
|
Flag input.Flag |
||||
|
} |
||||
|
|
||||
|
func (i *Int) Append(in string) error { |
||||
|
v, err := strconv.Atoi(in) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
i.Val = append(i.Val, v) |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (i *Int) Int() int { |
||||
|
if !i.Flag.IsArray() && len(i.Val) == 1 { |
||||
|
return i.Val[0] |
||||
|
} |
||||
|
|
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
func (i *Int) Ints() []int { |
||||
|
return i.Val |
||||
|
} |
||||
|
|
||||
|
func (i *Int) Any() interface{} { |
||||
|
if i.Flag&input.ValueArray > 0 { |
||||
|
return i.Ints() |
||||
|
} |
||||
|
|
||||
|
return i.Int() |
||||
|
} |
@ -0,0 +1,44 @@ |
|||||
|
package value |
||||
|
|
||||
|
import ( |
||||
|
"strconv" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
) |
||||
|
|
||||
|
type Int64 struct { |
||||
|
Empty |
||||
|
Val []int64 |
||||
|
Flag input.Flag |
||||
|
} |
||||
|
|
||||
|
func (i *Int64) Int64() int64 { |
||||
|
if !i.Flag.IsArray() && len(i.Val) == 1 { |
||||
|
return i.Val[0] |
||||
|
} |
||||
|
|
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
func (i *Int64) Int64s() []int64 { |
||||
|
return i.Val |
||||
|
} |
||||
|
|
||||
|
func (i *Int64) Any() interface{} { |
||||
|
if i.Flag&input.ValueArray > 0 { |
||||
|
return i.Int64s() |
||||
|
} |
||||
|
|
||||
|
return i.Int64() |
||||
|
} |
||||
|
|
||||
|
func (i *Int64) Append(in string) error { |
||||
|
v, err := strconv.ParseInt(in, 10, 64) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
i.Val = append(i.Val, v) |
||||
|
|
||||
|
return nil |
||||
|
} |
@ -0,0 +1,17 @@ |
|||||
|
package value |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
) |
||||
|
|
||||
|
var _ input.AppendValue = (*Read)(nil) |
||||
|
|
||||
|
type Read struct { |
||||
|
input.Value |
||||
|
} |
||||
|
|
||||
|
func (r *Read) Append(string) error { |
||||
|
return fmt.Errorf("%w: read value", input.ErrInvalidName) |
||||
|
} |
@ -0,0 +1,39 @@ |
|||||
|
package value |
||||
|
|
||||
|
import "gitoa.ru/go-4devs/console/input" |
||||
|
|
||||
|
type String struct { |
||||
|
Empty |
||||
|
Val []string |
||||
|
Flag input.Flag |
||||
|
} |
||||
|
|
||||
|
func (s *String) Append(in string) error { |
||||
|
s.Val = append(s.Val, in) |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (s *String) String() string { |
||||
|
if s.Flag.IsArray() { |
||||
|
return "" |
||||
|
} |
||||
|
|
||||
|
if len(s.Val) == 1 { |
||||
|
return s.Val[0] |
||||
|
} |
||||
|
|
||||
|
return "" |
||||
|
} |
||||
|
|
||||
|
func (s *String) Strings() []string { |
||||
|
return s.Val |
||||
|
} |
||||
|
|
||||
|
func (s *String) Any() interface{} { |
||||
|
if s.Flag.IsArray() { |
||||
|
return s.Strings() |
||||
|
} |
||||
|
|
||||
|
return s.String() |
||||
|
} |
@ -0,0 +1,44 @@ |
|||||
|
package value |
||||
|
|
||||
|
import ( |
||||
|
"time" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
) |
||||
|
|
||||
|
type Time struct { |
||||
|
Empty |
||||
|
Val []time.Time |
||||
|
Flag input.Flag |
||||
|
} |
||||
|
|
||||
|
func (t *Time) Append(in string) error { |
||||
|
v, err := time.Parse(time.RFC3339, in) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
t.Val = append(t.Val, v) |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (t *Time) Time() time.Time { |
||||
|
if !t.Flag.IsArray() && len(t.Val) == 1 { |
||||
|
return t.Val[0] |
||||
|
} |
||||
|
|
||||
|
return time.Time{} |
||||
|
} |
||||
|
|
||||
|
func (t *Time) Times() []time.Time { |
||||
|
return t.Val |
||||
|
} |
||||
|
|
||||
|
func (t *Time) Amy() interface{} { |
||||
|
if t.Flag&input.ValueArray > 0 { |
||||
|
return t.Times() |
||||
|
} |
||||
|
|
||||
|
return t.Time() |
||||
|
} |
@ -0,0 +1,44 @@ |
|||||
|
package value |
||||
|
|
||||
|
import ( |
||||
|
"strconv" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
) |
||||
|
|
||||
|
type Uint struct { |
||||
|
Empty |
||||
|
Val []uint |
||||
|
Flag input.Flag |
||||
|
} |
||||
|
|
||||
|
func (u *Uint) Append(in string) error { |
||||
|
v, err := strconv.ParseUint(in, 10, 64) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
u.Val = append(u.Val, uint(v)) |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (u *Uint) Uint() uint { |
||||
|
if !u.Flag.IsArray() && len(u.Val) == 1 { |
||||
|
return u.Val[0] |
||||
|
} |
||||
|
|
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
func (u *Uint) Uints() []uint { |
||||
|
return u.Val |
||||
|
} |
||||
|
|
||||
|
func (u *Uint) Any() interface{} { |
||||
|
if u.Flag&input.ValueArray > 0 { |
||||
|
return u.Uints() |
||||
|
} |
||||
|
|
||||
|
return u.Uint() |
||||
|
} |
@ -0,0 +1,44 @@ |
|||||
|
package value |
||||
|
|
||||
|
import ( |
||||
|
"strconv" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
) |
||||
|
|
||||
|
type Uint64 struct { |
||||
|
Empty |
||||
|
Val []uint64 |
||||
|
Flag input.Flag |
||||
|
} |
||||
|
|
||||
|
func (u *Uint64) Append(in string) error { |
||||
|
v, err := strconv.ParseUint(in, 10, 64) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
u.Val = append(u.Val, v) |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (u *Uint64) Uint64() uint64 { |
||||
|
if !u.Flag.IsArray() && len(u.Val) == 1 { |
||||
|
return u.Val[0] |
||||
|
} |
||||
|
|
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
func (u *Uint64) Uint64s() []uint64 { |
||||
|
return u.Val |
||||
|
} |
||||
|
|
||||
|
func (u *Uint64) Any() interface{} { |
||||
|
if u.Flag&input.ValueArray > 0 { |
||||
|
return u.Uint64s() |
||||
|
} |
||||
|
|
||||
|
return u.Uint64() |
||||
|
} |
@ -0,0 +1,84 @@ |
|||||
|
package value |
||||
|
|
||||
|
import ( |
||||
|
"time" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
) |
||||
|
|
||||
|
//nolint: gocyclo
|
||||
|
func New(v interface{}) input.Value { |
||||
|
switch val := v.(type) { |
||||
|
case string: |
||||
|
return &String{Val: []string{val}, Flag: input.ValueString} |
||||
|
case int: |
||||
|
return &Int{Val: []int{val}, Flag: input.ValueInt} |
||||
|
case int64: |
||||
|
return &Int64{Val: []int64{val}, Flag: input.ValueInt64} |
||||
|
case uint: |
||||
|
return &Uint{Val: []uint{val}, Flag: input.ValueUint} |
||||
|
case uint64: |
||||
|
return &Uint64{Val: []uint64{val}, Flag: input.ValueUint64} |
||||
|
case float64: |
||||
|
return &Float64{Val: []float64{val}, Flag: input.ValueFloat64} |
||||
|
case bool: |
||||
|
return &Bool{Val: []bool{val}, Flag: input.ValueBool} |
||||
|
case time.Duration: |
||||
|
return &Duration{Val: []time.Duration{val}, Flag: input.ValueDuration} |
||||
|
case time.Time: |
||||
|
return &Time{Val: []time.Time{val}, Flag: input.ValueTime} |
||||
|
case []int64: |
||||
|
return &Int64{Val: val, Flag: input.ValueInt64 | input.ValueArray} |
||||
|
case []uint: |
||||
|
return &Uint{Val: val, Flag: input.ValueUint | input.ValueArray} |
||||
|
case []uint64: |
||||
|
return &Uint64{Val: val, Flag: input.ValueUint64 | input.ValueArray} |
||||
|
case []float64: |
||||
|
return &Float64{Val: val, Flag: input.ValueFloat64 | input.ValueArray} |
||||
|
case []bool: |
||||
|
return &Bool{Val: val, Flag: input.ValueBool | input.ValueArray} |
||||
|
case []time.Duration: |
||||
|
return &Duration{Val: val, Flag: input.ValueDuration | input.ValueArray} |
||||
|
case []time.Time: |
||||
|
return &Time{Val: val, Flag: input.ValueTime | input.ValueArray} |
||||
|
case []string: |
||||
|
return &String{Val: val, Flag: input.ValueString | input.ValueArray} |
||||
|
case []int: |
||||
|
return &Int{Val: val, Flag: input.ValueInt | input.ValueArray} |
||||
|
case []interface{}: |
||||
|
return &Any{Val: val, Flag: input.ValueAny | input.ValueArray} |
||||
|
case input.Value: |
||||
|
return val |
||||
|
default: |
||||
|
if v != nil { |
||||
|
return &Any{Val: []interface{}{v}, Flag: input.ValueAny} |
||||
|
} |
||||
|
|
||||
|
return &Empty{} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func ByFlag(flag input.Flag) input.AppendValue { |
||||
|
switch { |
||||
|
case flag.IsInt(): |
||||
|
return &Int{Flag: flag | input.ValueInt} |
||||
|
case flag.IsInt64(): |
||||
|
return &Int64{Flag: flag | input.ValueInt64} |
||||
|
case flag.IsUint(): |
||||
|
return &Uint{Flag: flag | input.ValueUint} |
||||
|
case flag.IsUint64(): |
||||
|
return &Uint64{Flag: flag | input.ValueUint64} |
||||
|
case flag.IsFloat64(): |
||||
|
return &Float64{Flag: flag | input.ValueFloat64} |
||||
|
case flag.IsBool(): |
||||
|
return &Bool{Flag: flag | input.ValueBool} |
||||
|
case flag.IsDuration(): |
||||
|
return &Duration{Flag: flag | input.ValueDuration} |
||||
|
case flag.IsTime(): |
||||
|
return &Time{Flag: flag | input.ValueTime} |
||||
|
case flag.IsAny(): |
||||
|
return &Any{Flag: flag | input.ValueAny} |
||||
|
default: |
||||
|
return &String{} |
||||
|
} |
||||
|
} |
@ -0,0 +1,105 @@ |
|||||
|
package wrap |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"errors" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/input/value" |
||||
|
) |
||||
|
|
||||
|
type Input struct { |
||||
|
input.ReadInput |
||||
|
} |
||||
|
|
||||
|
func (i *Input) Option(ctx context.Context, name string) input.Value { |
||||
|
if v, err := i.ReadOption(ctx, name); err == nil { |
||||
|
return v |
||||
|
} |
||||
|
|
||||
|
return &value.Empty{} |
||||
|
} |
||||
|
|
||||
|
func (i *Input) Argument(ctx context.Context, name string) input.Value { |
||||
|
if v, err := i.ReadArgument(ctx, name); err == nil { |
||||
|
return v |
||||
|
} |
||||
|
|
||||
|
return &value.Empty{} |
||||
|
} |
||||
|
|
||||
|
func (i *Input) Bind(ctx context.Context, def *input.Definition) error { |
||||
|
if err := i.ReadInput.Bind(ctx, def); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
if err := i.bindArguments(ctx, def); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
return i.bindOptions(ctx, def) |
||||
|
} |
||||
|
|
||||
|
func (i *Input) bindOptions(ctx context.Context, def *input.Definition) error { |
||||
|
for _, name := range def.Options() { |
||||
|
opt, err := def.Option(name) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
v, err := i.ReadOption(ctx, name) |
||||
|
if err != nil && !errors.Is(err, input.ErrNotFound) { |
||||
|
return input.ErrorOption(name, err) |
||||
|
} |
||||
|
|
||||
|
if err == nil { |
||||
|
if err := opt.Validate(v); err != nil { |
||||
|
return input.ErrorOption(name, err) |
||||
|
} |
||||
|
|
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
if opt.IsRequired() && !opt.HasDefault() { |
||||
|
return input.ErrorOption(name, input.ErrRequired) |
||||
|
} |
||||
|
|
||||
|
if opt.HasDefault() { |
||||
|
i.SetOption(name, opt.Default) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (i *Input) bindArguments(ctx context.Context, def *input.Definition) error { |
||||
|
for pos, name := range def.Arguments() { |
||||
|
arg, err := def.Argument(pos) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
v, err := i.ReadArgument(ctx, name) |
||||
|
if err != nil && !errors.Is(err, input.ErrNotFound) { |
||||
|
return input.ErrorArgument(name, err) |
||||
|
} |
||||
|
|
||||
|
if err == nil { |
||||
|
if err := arg.Validate(v); err != nil { |
||||
|
return input.ErrorArgument(name, err) |
||||
|
} |
||||
|
|
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
if arg.IsRequired() && !arg.HasDefault() { |
||||
|
return input.ErrorArgument(name, input.ErrRequired) |
||||
|
} |
||||
|
|
||||
|
if arg.HasDefault() { |
||||
|
i.SetArgument(name, arg.Default) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
@ -0,0 +1,111 @@ |
|||||
|
package console |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"fmt" |
||||
|
"strings" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/descriptor" |
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/input/option" |
||||
|
"gitoa.ru/go-4devs/console/output" |
||||
|
"gitoa.ru/go-4devs/console/validator" |
||||
|
) |
||||
|
|
||||
|
const defaultLenNamespace = 2 |
||||
|
|
||||
|
//nolint: gochecknoinits
|
||||
|
func init() { |
||||
|
MustRegister(list()) |
||||
|
} |
||||
|
|
||||
|
func list() *Command { |
||||
|
return &Command{ |
||||
|
Name: CommandList, |
||||
|
Description: "Lists commands", |
||||
|
Help: ` |
||||
|
The <info>{{ .Name }}</info> command lists all commands: |
||||
|
<info>{{ .Bin }} {{ .Name }}</info> |
||||
|
You can also display the commands for a specific namespace: |
||||
|
<info>{{ .Bin }} {{ .Name }} test</info> |
||||
|
You can also output the information in other formats by using the <comment>--format</comment> option: |
||||
|
<info>{{ .Bin }} {{ .Name }} --format=xml</info> |
||||
|
`, |
||||
|
Execute: func(ctx context.Context, in input.Input, out output.Output) error { |
||||
|
ns := in.Argument(ctx, "namespace").String() |
||||
|
format := in.Option(ctx, helpOptFormat).String() |
||||
|
|
||||
|
des, err := descriptor.Find(format) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
cmds := Commands() |
||||
|
commands := descriptor.Commands{ |
||||
|
Namespace: ns, |
||||
|
Definition: Default(input.NewDefinition()), |
||||
|
} |
||||
|
groups := make(map[string]*descriptor.NSCommand) |
||||
|
namespaces := make([]string, 0, len(cmds)) |
||||
|
empty := descriptor.NSCommand{} |
||||
|
|
||||
|
for _, name := range cmds { |
||||
|
if ns != "" && !strings.HasPrefix(name, ns+":") { |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
cmd, _ := Find(name) |
||||
|
if cmd.Hidden { |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
gn := strings.SplitN(name, ":", 2) |
||||
|
if len(gn) != defaultLenNamespace { |
||||
|
empty.Append(cmd.Name, cmd.Description) |
||||
|
|
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
if _, ok := groups[gn[0]]; !ok { |
||||
|
groups[gn[0]] = &descriptor.NSCommand{ |
||||
|
Name: gn[0], |
||||
|
} |
||||
|
namespaces = append(namespaces, gn[0]) |
||||
|
} |
||||
|
|
||||
|
groups[gn[0]].Append(name, cmd.Description) |
||||
|
} |
||||
|
if len(empty.Commands) > 0 { |
||||
|
commands.Commands = append(commands.Commands, empty) |
||||
|
} |
||||
|
|
||||
|
for _, name := range namespaces { |
||||
|
commands.Commands = append(commands.Commands, *groups[name]) |
||||
|
} |
||||
|
|
||||
|
if ns != "" && len(commands.Commands) == 0 { |
||||
|
return fmt.Errorf("%w: namespace %s", ErrNotFound, ns) |
||||
|
} |
||||
|
|
||||
|
return des.Commands(ctx, out, commands) |
||||
|
}, |
||||
|
Configure: func(ctx context.Context, config *input.Definition) error { |
||||
|
formats := descriptor.Descriptors() |
||||
|
config. |
||||
|
SetArguments( |
||||
|
input.NewArgument("namespace", "The namespace name"), |
||||
|
). |
||||
|
SetOptions( |
||||
|
input.NewOption(helpOptFormat, fmt.Sprintf("The output format (%s)", strings.Join(formats, ", ")), |
||||
|
option.Required, |
||||
|
option.Default(formats[0]), |
||||
|
option.Valid( |
||||
|
validator.NotBlank(0), |
||||
|
validator.Enum(formats...), |
||||
|
), |
||||
|
), |
||||
|
) |
||||
|
|
||||
|
return nil |
||||
|
}, |
||||
|
} |
||||
|
} |
@ -0,0 +1,5 @@ |
|||||
|
package formatter |
||||
|
|
||||
|
func Ansi() *Formatter { |
||||
|
return New() |
||||
|
} |
@ -0,0 +1,79 @@ |
|||||
|
package formatter |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"context" |
||||
|
"regexp" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/output/style" |
||||
|
) |
||||
|
|
||||
|
//nolint: gochecknoglobals
|
||||
|
var re = regexp.MustCompile(`<(([a-z][^<>]+)|/([a-z][^<>]+)?)>`) |
||||
|
|
||||
|
func WithStyle(styles func(string) (style.Style, error)) func(*Formatter) { |
||||
|
return func(f *Formatter) { |
||||
|
f.styles = styles |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func New(opts ...func(*Formatter)) *Formatter { |
||||
|
f := &Formatter{ |
||||
|
styles: style.Find, |
||||
|
} |
||||
|
|
||||
|
for _, opt := range opts { |
||||
|
opt(f) |
||||
|
} |
||||
|
|
||||
|
return f |
||||
|
} |
||||
|
|
||||
|
type Formatter struct { |
||||
|
styles func(string) (style.Style, error) |
||||
|
} |
||||
|
|
||||
|
func (a *Formatter) Format(ctx context.Context, msg string) string { |
||||
|
var ( |
||||
|
out bytes.Buffer |
||||
|
cur int |
||||
|
) |
||||
|
|
||||
|
for _, idx := range re.FindAllStringIndex(msg, -1) { |
||||
|
tag := msg[idx[0]+1 : idx[1]-1] |
||||
|
|
||||
|
if cur < idx[0] { |
||||
|
out.WriteString(msg[cur:idx[0]]) |
||||
|
} |
||||
|
|
||||
|
var ( |
||||
|
st style.Style |
||||
|
err error |
||||
|
) |
||||
|
|
||||
|
switch { |
||||
|
case tag[0:1] == "/": |
||||
|
st, err = a.styles(tag[1:]) |
||||
|
if err == nil { |
||||
|
out.WriteString(st.Set(style.ActionUnset)) |
||||
|
} |
||||
|
default: |
||||
|
st, err = a.styles(tag) |
||||
|
if err == nil { |
||||
|
out.WriteString(st.Set(style.ActionSet)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if err != nil { |
||||
|
cur = idx[0] |
||||
|
} else { |
||||
|
cur = idx[1] |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if len(msg) > cur { |
||||
|
out.WriteString(msg[cur:]) |
||||
|
} |
||||
|
|
||||
|
return out.String() |
||||
|
} |
@ -0,0 +1,26 @@ |
|||||
|
package formatter_test |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"testing" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/output/formatter" |
||||
|
) |
||||
|
|
||||
|
func TestFormatter(t *testing.T) { |
||||
|
ctx := context.Background() |
||||
|
formatter := formatter.New() |
||||
|
|
||||
|
cases := map[string]string{ |
||||
|
"<info>info message</info>": "\x1b[32minfo message\x1b[39m", |
||||
|
"<info><command></info>": "\x1b[32m<command>\x1b[39m", |
||||
|
"<html>...</html>": "<html>...</html>", |
||||
|
} |
||||
|
|
||||
|
for msg, ex := range cases { |
||||
|
got := formatter.Format(ctx, msg) |
||||
|
if ex != got { |
||||
|
t.Errorf("ivalid expected:%#v, got: %#v", ex, got) |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,14 @@ |
|||||
|
package formatter |
||||
|
|
||||
|
import "gitoa.ru/go-4devs/console/output/style" |
||||
|
|
||||
|
func None() *Formatter { |
||||
|
return New( |
||||
|
WithStyle(func(name string) (style.Style, error) { |
||||
|
if _, err := style.Find(name); err != nil { |
||||
|
return style.Empty(), err |
||||
|
} |
||||
|
|
||||
|
return style.Empty(), nil |
||||
|
})) |
||||
|
} |
@ -0,0 +1,27 @@ |
|||||
|
package formatter_test |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"testing" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/output/formatter" |
||||
|
) |
||||
|
|
||||
|
func TestNone(t *testing.T) { |
||||
|
ctx := context.Background() |
||||
|
none := formatter.None() |
||||
|
|
||||
|
cases := map[string]string{ |
||||
|
"<info>message info</info>": "message info", |
||||
|
"<error>message error</error>": "message error", |
||||
|
"<comment><scheme></comment>": "<scheme>", |
||||
|
"<body>body</body>": "<body>body</body>", |
||||
|
} |
||||
|
|
||||
|
for msg, ex := range cases { |
||||
|
got := none.Format(ctx, msg) |
||||
|
if ex != got { |
||||
|
t.Errorf("expect:%#v, got:%#v", ex, got) |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,59 @@ |
|||||
|
package output |
||||
|
|
||||
|
type Key string |
||||
|
|
||||
|
func (k Key) Any(v interface{}) KeyValue { |
||||
|
return KeyValue{ |
||||
|
Key: k, |
||||
|
Value: AnyValue(v), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (k Key) Bool(v bool) KeyValue { |
||||
|
return KeyValue{ |
||||
|
Key: k, |
||||
|
Value: BoolValue(v), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (k Key) Int(v int) KeyValue { |
||||
|
return KeyValue{ |
||||
|
Key: k, |
||||
|
Value: IntValue(v), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (k Key) Int64(v int64) KeyValue { |
||||
|
return KeyValue{ |
||||
|
Key: k, |
||||
|
Value: Int64Value(v), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (k Key) Uint(v uint) KeyValue { |
||||
|
return KeyValue{ |
||||
|
Key: k, |
||||
|
Value: UintValue(v), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (k Key) Uint64(v uint64) KeyValue { |
||||
|
return KeyValue{ |
||||
|
Key: k, |
||||
|
Value: Uint64Value(v), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (k Key) Float64(v float64) KeyValue { |
||||
|
return KeyValue{ |
||||
|
Key: k, |
||||
|
Value: Float64Value(v), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (k Key) String(v string) KeyValue { |
||||
|
return KeyValue{ |
||||
|
Key: k, |
||||
|
Value: StringValue(v), |
||||
|
} |
||||
|
} |
@ -0,0 +1,63 @@ |
|||||
|
package output |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"strings" |
||||
|
) |
||||
|
|
||||
|
var ( |
||||
|
_ fmt.Stringer = KeyValue{} |
||||
|
_ fmt.Stringer = KeyValues{} |
||||
|
) |
||||
|
|
||||
|
type KeyValues []KeyValue |
||||
|
|
||||
|
func (kv KeyValues) String() string { |
||||
|
s := make([]string, len(kv)) |
||||
|
for i, v := range kv { |
||||
|
s[i] = v.String() |
||||
|
} |
||||
|
|
||||
|
return strings.Join(s, ", ") |
||||
|
} |
||||
|
|
||||
|
type KeyValue struct { |
||||
|
Key Key |
||||
|
Value Value |
||||
|
} |
||||
|
|
||||
|
func (k KeyValue) String() string { |
||||
|
return string(k.Key) + "=\"" + k.Value.String() + "\"" |
||||
|
} |
||||
|
|
||||
|
func Any(k string, v interface{}) KeyValue { |
||||
|
return Key(k).Any(v) |
||||
|
} |
||||
|
|
||||
|
func Bool(k string, v bool) KeyValue { |
||||
|
return Key(k).Bool(v) |
||||
|
} |
||||
|
|
||||
|
func Int(k string, v int) KeyValue { |
||||
|
return Key(k).Int(v) |
||||
|
} |
||||
|
|
||||
|
func Int64(k string, v int64) KeyValue { |
||||
|
return Key(k).Int64(v) |
||||
|
} |
||||
|
|
||||
|
func Uint(k string, v uint) KeyValue { |
||||
|
return Key(k).Uint(v) |
||||
|
} |
||||
|
|
||||
|
func Uint64(k string, v uint64) KeyValue { |
||||
|
return Key(k).Uint64(v) |
||||
|
} |
||||
|
|
||||
|
func Float64(k string, v float64) KeyValue { |
||||
|
return Key(k).Float64(v) |
||||
|
} |
||||
|
|
||||
|
func String(k string, v string) KeyValue { |
||||
|
return Key(k).String(v) |
||||
|
} |
@ -0,0 +1,77 @@ |
|||||
|
package output |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
) |
||||
|
|
||||
|
type Verbosity int |
||||
|
|
||||
|
const ( |
||||
|
VerbosityQuiet Verbosity = iota - 1 |
||||
|
VerbosityNorm |
||||
|
VerbosityInfo |
||||
|
VerbosityDebug |
||||
|
VerbosityTrace |
||||
|
) |
||||
|
|
||||
|
type Output func(ctx context.Context, verb Verbosity, msg string, args ...KeyValue) (int, error) |
||||
|
|
||||
|
func (o Output) Print(ctx context.Context, args ...interface{}) { |
||||
|
o(ctx, VerbosityNorm, fmt.Sprint(args...)) |
||||
|
} |
||||
|
|
||||
|
func (o Output) PrintKV(ctx context.Context, msg string, kv ...KeyValue) { |
||||
|
o(ctx, VerbosityNorm, msg, kv...) |
||||
|
} |
||||
|
|
||||
|
func (o Output) Printf(ctx context.Context, format string, args ...interface{}) { |
||||
|
o(ctx, VerbosityNorm, fmt.Sprintf(format, args...)) |
||||
|
} |
||||
|
|
||||
|
func (o Output) Println(ctx context.Context, args ...interface{}) { |
||||
|
o(ctx, VerbosityNorm, fmt.Sprintln(args...)) |
||||
|
} |
||||
|
|
||||
|
func (o Output) Info(ctx context.Context, args ...interface{}) { |
||||
|
o(ctx, VerbosityInfo, fmt.Sprint(args...)) |
||||
|
} |
||||
|
|
||||
|
func (o Output) InfoKV(ctx context.Context, msg string, kv ...KeyValue) { |
||||
|
o(ctx, VerbosityInfo, msg, kv...) |
||||
|
} |
||||
|
|
||||
|
func (o Output) Debug(ctx context.Context, args ...interface{}) { |
||||
|
o(ctx, VerbosityDebug, fmt.Sprint(args...)) |
||||
|
} |
||||
|
|
||||
|
func (o Output) DebugKV(ctx context.Context, msg string, kv ...KeyValue) { |
||||
|
o(ctx, VerbosityDebug, msg, kv...) |
||||
|
} |
||||
|
|
||||
|
func (o Output) Trace(ctx context.Context, args ...interface{}) { |
||||
|
o(ctx, VerbosityTrace, fmt.Sprint(args...)) |
||||
|
} |
||||
|
|
||||
|
func (o Output) TraceKV(ctx context.Context, msg string, kv ...KeyValue) { |
||||
|
o(ctx, VerbosityTrace, msg, kv...) |
||||
|
} |
||||
|
|
||||
|
func (o Output) Write(b []byte) (int, error) { |
||||
|
return o(context.Background(), VerbosityNorm, string(b)) |
||||
|
} |
||||
|
|
||||
|
func (o Output) Writer(ctx context.Context, verb Verbosity) io.Writer { |
||||
|
return verbosityWriter{ctx, o, verb} |
||||
|
} |
||||
|
|
||||
|
type verbosityWriter struct { |
||||
|
ctx context.Context |
||||
|
out Output |
||||
|
verb Verbosity |
||||
|
} |
||||
|
|
||||
|
func (w verbosityWriter) Write(b []byte) (int, error) { |
||||
|
return w.out(w.ctx, w.verb, string(b)) |
||||
|
} |
@ -0,0 +1,51 @@ |
|||||
|
package style |
||||
|
|
||||
|
const ( |
||||
|
Black Color = "0" |
||||
|
Red Color = "1" |
||||
|
Green Color = "2" |
||||
|
Yellow Color = "3" |
||||
|
Blue Color = "4" |
||||
|
Magenta Color = "5" |
||||
|
Cyan Color = "6" |
||||
|
White Color = "7" |
||||
|
Default Color = "9" |
||||
|
) |
||||
|
|
||||
|
const ( |
||||
|
Bold Option = "122" |
||||
|
Underscore Option = "424" |
||||
|
Blink Option = "525" |
||||
|
Reverse Option = "727" |
||||
|
Conseal Option = "828" |
||||
|
) |
||||
|
|
||||
|
const ( |
||||
|
ActionSet = 1 |
||||
|
ActionUnset = 2 |
||||
|
) |
||||
|
|
||||
|
type Option string |
||||
|
|
||||
|
func (o Option) Apply(action int) string { |
||||
|
v := string(o) |
||||
|
|
||||
|
switch action { |
||||
|
case ActionSet: |
||||
|
return v[0:1] |
||||
|
case ActionUnset: |
||||
|
return v[1:] |
||||
|
} |
||||
|
|
||||
|
return "" |
||||
|
} |
||||
|
|
||||
|
type Color string |
||||
|
|
||||
|
func (c Color) Apply(action int) string { |
||||
|
if action == ActionSet { |
||||
|
return string(c) |
||||
|
} |
||||
|
|
||||
|
return string(Default) |
||||
|
} |
@ -0,0 +1,88 @@ |
|||||
|
package style |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
"strings" |
||||
|
"sync" |
||||
|
) |
||||
|
|
||||
|
//nolint: gochecknoglobals
|
||||
|
var ( |
||||
|
styles = map[string]Style{ |
||||
|
"error": {Foreground: White, Background: Red}, |
||||
|
"info": {Foreground: Green}, |
||||
|
"comment": {Foreground: Yellow}, |
||||
|
"question": {Foreground: Black, Background: Cyan}, |
||||
|
} |
||||
|
stylesMu sync.Mutex |
||||
|
empty = Style{} |
||||
|
) |
||||
|
|
||||
|
var ( |
||||
|
ErrNotFound = errors.New("console: style not found") |
||||
|
ErrDuplicateStyle = errors.New("console: Register called twice") |
||||
|
) |
||||
|
|
||||
|
func Empty() Style { |
||||
|
return empty |
||||
|
} |
||||
|
|
||||
|
func Find(name string) (Style, error) { |
||||
|
if st, has := styles[name]; has { |
||||
|
return st, nil |
||||
|
} |
||||
|
|
||||
|
return empty, ErrNotFound |
||||
|
} |
||||
|
|
||||
|
func Register(name string, style Style) error { |
||||
|
stylesMu.Lock() |
||||
|
defer stylesMu.Unlock() |
||||
|
|
||||
|
if _, has := styles[name]; has { |
||||
|
return fmt.Errorf("%w for style %s", ErrDuplicateStyle, name) |
||||
|
} |
||||
|
|
||||
|
styles[name] = style |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func MustRegister(name string, style Style) { |
||||
|
if err := Register(name, style); err != nil { |
||||
|
panic(err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
type Style struct { |
||||
|
Background Color |
||||
|
Foreground Color |
||||
|
Options []Option |
||||
|
} |
||||
|
|
||||
|
func (s Style) Apply(msg string) string { |
||||
|
return s.Set(ActionSet) + msg + s.Set(ActionUnset) |
||||
|
} |
||||
|
|
||||
|
func (s Style) Set(action int) string { |
||||
|
style := make([]string, 0, len(s.Options)) |
||||
|
|
||||
|
if s.Foreground != "" { |
||||
|
style = append(style, "3"+s.Foreground.Apply(action)) |
||||
|
} |
||||
|
|
||||
|
if s.Background != "" { |
||||
|
style = append(style, "4"+s.Background.Apply(action)) |
||||
|
} |
||||
|
|
||||
|
for _, opt := range s.Options { |
||||
|
style = append(style, opt.Apply(action)) |
||||
|
} |
||||
|
|
||||
|
if len(style) == 0 { |
||||
|
return "" |
||||
|
} |
||||
|
|
||||
|
return "\033[" + strings.Join(style, ";") + "m" |
||||
|
} |
@ -0,0 +1,57 @@ |
|||||
|
package output |
||||
|
|
||||
|
import "fmt" |
||||
|
|
||||
|
type Type int |
||||
|
|
||||
|
const ( |
||||
|
TypeAny Type = iota |
||||
|
TypeBool |
||||
|
TypeInt |
||||
|
TypeInt64 |
||||
|
TypeUint |
||||
|
TypeUint64 |
||||
|
TypeFloat64 |
||||
|
TypeString |
||||
|
) |
||||
|
|
||||
|
type Value struct { |
||||
|
vtype Type |
||||
|
value interface{} |
||||
|
} |
||||
|
|
||||
|
func (v Value) String() string { |
||||
|
return fmt.Sprint(v.value) |
||||
|
} |
||||
|
|
||||
|
func AnyValue(v interface{}) Value { |
||||
|
return Value{vtype: TypeAny, value: v} |
||||
|
} |
||||
|
|
||||
|
func BoolValue(v bool) Value { |
||||
|
return Value{vtype: TypeBool, value: v} |
||||
|
} |
||||
|
|
||||
|
func IntValue(v int) Value { |
||||
|
return Value{vtype: TypeInt, value: v} |
||||
|
} |
||||
|
|
||||
|
func Int64Value(v int64) Value { |
||||
|
return Value{vtype: TypeInt64, value: v} |
||||
|
} |
||||
|
|
||||
|
func UintValue(v uint) Value { |
||||
|
return Value{vtype: TypeUint, value: v} |
||||
|
} |
||||
|
|
||||
|
func Uint64Value(v uint64) Value { |
||||
|
return Value{vtype: TypeUint64, value: v} |
||||
|
} |
||||
|
|
||||
|
func Float64Value(v float64) Value { |
||||
|
return Value{vtype: TypeFloat64, value: v} |
||||
|
} |
||||
|
|
||||
|
func StringValue(v string) Value { |
||||
|
return Value{vtype: TypeString, value: v} |
||||
|
} |
@ -0,0 +1,23 @@ |
|||||
|
package verbosity |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/output" |
||||
|
) |
||||
|
|
||||
|
func Verb(out output.Output, verb output.Verbosity) output.Output { |
||||
|
return func(ctx context.Context, v output.Verbosity, msg string, kv ...output.KeyValue) (int, error) { |
||||
|
if verb >= v { |
||||
|
return out(ctx, v, msg, kv...) |
||||
|
} |
||||
|
|
||||
|
return 0, nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func Quiet() output.Output { |
||||
|
return func(context.Context, output.Verbosity, string, ...output.KeyValue) (int, error) { |
||||
|
return 0, nil |
||||
|
} |
||||
|
} |
@ -0,0 +1,22 @@ |
|||||
|
package wrap |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/output" |
||||
|
"gitoa.ru/go-4devs/console/output/formatter" |
||||
|
) |
||||
|
|
||||
|
func Format(out output.Output, format *formatter.Formatter) output.Output { |
||||
|
return func(ctx context.Context, v output.Verbosity, msg string, kv ...output.KeyValue) (int, error) { |
||||
|
return out(ctx, v, format.Format(ctx, msg), kv...) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func Ansi(out output.Output) output.Output { |
||||
|
return Format(out, formatter.Ansi()) |
||||
|
} |
||||
|
|
||||
|
func None(out output.Output) output.Output { |
||||
|
return Format(out, formatter.None()) |
||||
|
} |
@ -0,0 +1,44 @@ |
|||||
|
package writer |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"context" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"os" |
||||
|
"strings" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/output" |
||||
|
) |
||||
|
|
||||
|
func Stderr() output.Output { |
||||
|
return New(os.Stderr, String) |
||||
|
} |
||||
|
|
||||
|
func Stdout() output.Output { |
||||
|
return New(os.Stdout, String) |
||||
|
} |
||||
|
|
||||
|
func Buffer(buf *bytes.Buffer) output.Output { |
||||
|
return New(buf, String) |
||||
|
} |
||||
|
|
||||
|
func String(_ output.Verbosity, msg string, kv ...output.KeyValue) string { |
||||
|
if len(kv) > 0 { |
||||
|
newline := "" |
||||
|
if msg[len(msg)-1:] == "\n" { |
||||
|
newline = "\n" |
||||
|
} |
||||
|
|
||||
|
return "msg=\"" + strings.TrimSpace(msg) + "\", " + output.KeyValues(kv).String() + newline |
||||
|
|
||||
|
} |
||||
|
|
||||
|
return msg |
||||
|
} |
||||
|
|
||||
|
func New(w io.Writer, format func(verb output.Verbosity, msg string, kv ...output.KeyValue) string) output.Output { |
||||
|
return func(ctx context.Context, verb output.Verbosity, msg string, kv ...output.KeyValue) (int, error) { |
||||
|
return fmt.Fprint(w, format(verb, msg, kv...)) |
||||
|
} |
||||
|
} |
@ -0,0 +1,49 @@ |
|||||
|
package writer_test |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"context" |
||||
|
"testing" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/output" |
||||
|
"gitoa.ru/go-4devs/console/output/writer" |
||||
|
) |
||||
|
|
||||
|
func TestNew(t *testing.T) { |
||||
|
ctx := context.Background() |
||||
|
buf := bytes.Buffer{} |
||||
|
wr := writer.New(&buf, writer.String) |
||||
|
|
||||
|
cases := map[string]struct { |
||||
|
ex string |
||||
|
kv []output.KeyValue |
||||
|
}{ |
||||
|
"message": { |
||||
|
ex: "message", |
||||
|
}, |
||||
|
"msg with kv": { |
||||
|
ex: "msg=\"msg with kv\", string key=\"string value\", bool key=\"false\", int key=\"42\"", |
||||
|
kv: []output.KeyValue{ |
||||
|
output.String("string key", "string value"), |
||||
|
output.Bool("bool key", false), |
||||
|
output.Int("int key", 42), |
||||
|
}, |
||||
|
}, |
||||
|
"msg with newline \n": { |
||||
|
ex: "msg=\"msg with newline\", int=\"42\"\n", |
||||
|
kv: []output.KeyValue{ |
||||
|
output.Int("int", 42), |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
for msg, data := range cases { |
||||
|
wr.InfoKV(ctx, msg, data.kv...) |
||||
|
|
||||
|
if data.ex != buf.String() { |
||||
|
t.Errorf("message not equals expext:%s, got:%s", data.ex, buf.String()) |
||||
|
} |
||||
|
|
||||
|
buf.Reset() |
||||
|
} |
||||
|
} |
@ -0,0 +1,141 @@ |
|||||
|
package console |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
"regexp" |
||||
|
"sort" |
||||
|
"strings" |
||||
|
"sync" |
||||
|
) |
||||
|
|
||||
|
const ( |
||||
|
CommandHelp = "help" |
||||
|
CommandList = "list" |
||||
|
) |
||||
|
|
||||
|
var ( |
||||
|
ErrNotFound = errors.New("command not found") |
||||
|
ErrCommandNil = errors.New("console: Register command is nil") |
||||
|
ErrCommandDuplicate = errors.New("console: duplicate command") |
||||
|
) |
||||
|
|
||||
|
//nolint: gochecknoglobals
|
||||
|
var ( |
||||
|
commandsMu sync.RWMutex |
||||
|
commands = make(map[string]*Command) |
||||
|
findCommand = regexp.MustCompile("([^:]+|)") |
||||
|
) |
||||
|
|
||||
|
type ErrorAlternatives struct { |
||||
|
alt []string |
||||
|
err error |
||||
|
} |
||||
|
|
||||
|
func (e ErrorAlternatives) Error() string { |
||||
|
return fmt.Sprintf("%s, alternatives: [%s]", e.err, strings.Join(e.alt, ",")) |
||||
|
} |
||||
|
|
||||
|
func (e ErrorAlternatives) Is(err error) bool { |
||||
|
return errors.Is(e.err, err) |
||||
|
} |
||||
|
|
||||
|
func (e ErrorAlternatives) Unwrap() error { |
||||
|
return e.err |
||||
|
} |
||||
|
|
||||
|
func (e ErrorAlternatives) Alternatives() []string { |
||||
|
return e.alt |
||||
|
} |
||||
|
|
||||
|
// MustRegister register command or panic if err.
|
||||
|
func MustRegister(cmd *Command) { |
||||
|
if err := Register(cmd); err != nil { |
||||
|
panic(err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Register makes a command available execute in app. If Register is called twice with the same name or if driver is nil, return error.
|
||||
|
func Register(cmd *Command) error { |
||||
|
if cmd == nil { |
||||
|
return ErrCommandNil |
||||
|
} |
||||
|
|
||||
|
if _, err := Find(cmd.Name); !errors.Is(err, ErrNotFound) { |
||||
|
return fmt.Errorf("%w: command %s", ErrCommandDuplicate, cmd.Name) |
||||
|
} |
||||
|
|
||||
|
register(cmd) |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func register(cmd *Command) { |
||||
|
commandsMu.Lock() |
||||
|
defer commandsMu.Unlock() |
||||
|
|
||||
|
if cmd != nil && cmd.Name != "" { |
||||
|
commands[cmd.Name] = cmd |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Commands returns a sorted list of the names of the registered commands.
|
||||
|
func Commands() []string { |
||||
|
commandsMu.RLock() |
||||
|
defer commandsMu.RUnlock() |
||||
|
|
||||
|
return commandNames() |
||||
|
} |
||||
|
|
||||
|
func commandNames() []string { |
||||
|
names := make([]string, 0, len(commands)) |
||||
|
for name := range commands { |
||||
|
names = append(names, name) |
||||
|
} |
||||
|
|
||||
|
sort.Strings(names) |
||||
|
|
||||
|
return names |
||||
|
} |
||||
|
|
||||
|
// Find command by name, tries to find the best match if you give it an abbreviation of a name.
|
||||
|
func Find(name string) (*Command, error) { |
||||
|
commandsMu.RLock() |
||||
|
defer commandsMu.RUnlock() |
||||
|
|
||||
|
if cmd, ok := commands[name]; ok { |
||||
|
return cmd, nil |
||||
|
} |
||||
|
|
||||
|
nameRegexp := findCommand.ReplaceAllStringFunc(name, func(in string) string { |
||||
|
return in + "[^:]*" |
||||
|
}) |
||||
|
|
||||
|
findCommands := make([]*Command, 0) |
||||
|
|
||||
|
cmdRegexp, err := regexp.Compile("^" + nameRegexp + "$") |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
for name := range commands { |
||||
|
if !commands[name].Hidden && cmdRegexp.MatchString(name) { |
||||
|
findCommands = append(findCommands, commands[name]) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if len(findCommands) == 1 { |
||||
|
return findCommands[0], nil |
||||
|
} |
||||
|
|
||||
|
if len(findCommands) > 1 { |
||||
|
names := make([]string, len(findCommands)) |
||||
|
for i := range findCommands { |
||||
|
names[i] = findCommands[i].Name |
||||
|
} |
||||
|
|
||||
|
return nil, ErrorAlternatives{alt: names, err: ErrNotFound} |
||||
|
} |
||||
|
|
||||
|
return nil, ErrNotFound |
||||
|
} |
@ -0,0 +1,30 @@ |
|||||
|
package console_test |
||||
|
|
||||
|
import ( |
||||
|
"testing" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console" |
||||
|
) |
||||
|
|
||||
|
func TestFind(t *testing.T) { |
||||
|
cases := map[string]string{ |
||||
|
"fdevs:console:test": "fdevs:console:test", |
||||
|
"fd:c:t": "fdevs:console:test", |
||||
|
"fd::t": "fdevs:console:test", |
||||
|
"f:c:t": "fdevs:console:test", |
||||
|
"f:c:a": "fdevs:console:arg", |
||||
|
} |
||||
|
|
||||
|
for name, ex := range cases { |
||||
|
res, err := console.Find(name) |
||||
|
if err != nil { |
||||
|
t.Errorf("expect <nil> err, got:%s", err) |
||||
|
|
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
if res.Name != ex { |
||||
|
t.Errorf("expect: %s, got: %s", ex, res) |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,16 @@ |
|||||
|
package validator |
||||
|
|
||||
|
import "gitoa.ru/go-4devs/console/input" |
||||
|
|
||||
|
func Enum(enum ...string) func(input.Value) error { |
||||
|
return func(in input.Value) error { |
||||
|
v := in.String() |
||||
|
for _, e := range enum { |
||||
|
if e == v { |
||||
|
return nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return NewError(ErrInvalid, v, enum) |
||||
|
} |
||||
|
} |
@ -0,0 +1,24 @@ |
|||||
|
package validator_test |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"testing" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input/value" |
||||
|
"gitoa.ru/go-4devs/console/validator" |
||||
|
) |
||||
|
|
||||
|
func TestEnum(t *testing.T) { |
||||
|
validValue := value.New("valid") |
||||
|
invalidValue := value.New("invalid") |
||||
|
|
||||
|
enum := validator.Enum("valid", "other", "three") |
||||
|
|
||||
|
if err := enum(validValue); err != nil { |
||||
|
t.Errorf("expected valid value got err:%s", err) |
||||
|
} |
||||
|
|
||||
|
if err := enum(invalidValue); !errors.Is(err, validator.ErrInvalid) { |
||||
|
t.Errorf("expected err:%s, got: %s", validator.ErrInvalid, err) |
||||
|
} |
||||
|
} |
@ -0,0 +1,37 @@ |
|||||
|
package validator |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
) |
||||
|
|
||||
|
var ( |
||||
|
ErrInvalid = errors.New("invalid value") |
||||
|
ErrNotBlank = errors.New("not blank") |
||||
|
) |
||||
|
|
||||
|
func NewError(err error, value, expect interface{}) Error { |
||||
|
return Error{ |
||||
|
err: err, |
||||
|
value: value, |
||||
|
expect: expect, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
type Error struct { |
||||
|
err error |
||||
|
value interface{} |
||||
|
expect interface{} |
||||
|
} |
||||
|
|
||||
|
func (e Error) Error() string { |
||||
|
return fmt.Sprintf("%s: expext: %s, given: %s", e.err, e.expect, e.value) |
||||
|
} |
||||
|
|
||||
|
func (e Error) Is(err error) bool { |
||||
|
return errors.Is(e.err, err) |
||||
|
} |
||||
|
|
||||
|
func (e Error) Unwrap() error { |
||||
|
return e.err |
||||
|
} |
@ -0,0 +1,109 @@ |
|||||
|
package validator |
||||
|
|
||||
|
import ( |
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
) |
||||
|
|
||||
|
//nolint: gocyclo
|
||||
|
func NotBlank(flag input.Flag) func(input.Value) error { |
||||
|
return func(in input.Value) error { |
||||
|
switch { |
||||
|
case flag.IsAny() && in.Any() != nil: |
||||
|
return nil |
||||
|
case flag.IsArray(): |
||||
|
return arrayNotBlank(flag, in) |
||||
|
case flag.IsInt() && in.Int() != 0: |
||||
|
return nil |
||||
|
case flag.IsInt64() && in.Int64() != 0: |
||||
|
return nil |
||||
|
case flag.IsUint() && in.Uint() != 0: |
||||
|
return nil |
||||
|
case flag.IsUint64() && in.Uint64() != 0: |
||||
|
return nil |
||||
|
case flag.IsFloat64() && in.Float64() != 0: |
||||
|
return nil |
||||
|
case flag.IsDuration() && in.Duration() != 0: |
||||
|
return nil |
||||
|
case flag.IsTime() && !in.Time().IsZero(): |
||||
|
return nil |
||||
|
case flag.IsString() && len(in.String()) > 0: |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
return ErrNotBlank |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//nolint: gocyclo,gocognit
|
||||
|
func arrayNotBlank(flag input.Flag, in input.Value) error { |
||||
|
switch { |
||||
|
case flag.IsInt() && len(in.Ints()) > 0: |
||||
|
for _, i := range in.Ints() { |
||||
|
if i == 0 { |
||||
|
return ErrNotBlank |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
case flag.IsInt64() && len(in.Int64s()) > 0: |
||||
|
for _, i := range in.Int64s() { |
||||
|
if i == 0 { |
||||
|
return ErrNotBlank |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
case flag.IsUint() && len(in.Uints()) > 0: |
||||
|
for _, u := range in.Uints() { |
||||
|
if u == 0 { |
||||
|
return ErrNotBlank |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
case flag.IsUint64() && len(in.Uint64s()) > 0: |
||||
|
for _, u := range in.Uint64s() { |
||||
|
if u == 0 { |
||||
|
return ErrNotBlank |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
case flag.IsFloat64() && len(in.Float64s()) > 0: |
||||
|
for _, f := range in.Float64s() { |
||||
|
if f == 0 { |
||||
|
return ErrNotBlank |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
case flag.IsBool() && len(in.Bools()) > 0: |
||||
|
return nil |
||||
|
case flag.IsDuration() && len(in.Durations()) > 0: |
||||
|
for _, d := range in.Durations() { |
||||
|
if d == 0 { |
||||
|
return ErrNotBlank |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
case flag.IsTime() && len(in.Times()) > 0: |
||||
|
for _, t := range in.Times() { |
||||
|
if t.IsZero() { |
||||
|
return ErrNotBlank |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
case flag.IsString() && len(in.Strings()) > 0: |
||||
|
for _, st := range in.Strings() { |
||||
|
if len(st) == 0 { |
||||
|
return ErrNotBlank |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
return ErrNotBlank |
||||
|
} |
@ -0,0 +1,109 @@ |
|||||
|
package validator_test |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"testing" |
||||
|
"time" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/input/value" |
||||
|
"gitoa.ru/go-4devs/console/validator" |
||||
|
) |
||||
|
|
||||
|
func TestNotBlank(t *testing.T) { |
||||
|
cases := map[string]struct { |
||||
|
flag input.Flag |
||||
|
value input.Value |
||||
|
empty input.Value |
||||
|
}{ |
||||
|
"any": {flag: input.ValueAny, value: value.New(float32(1))}, |
||||
|
"array int": { |
||||
|
flag: input.ValueInt | input.ValueArray, |
||||
|
value: value.New([]int{1}), |
||||
|
empty: value.New([]int{10, 20, 0}), |
||||
|
}, |
||||
|
"array int64": { |
||||
|
flag: input.ValueInt64 | input.ValueArray, |
||||
|
value: value.New([]int64{1}), |
||||
|
empty: value.New([]int64{0}), |
||||
|
}, |
||||
|
"array uint": { |
||||
|
flag: input.ValueUint | input.ValueArray, |
||||
|
value: value.New([]uint{1}), |
||||
|
empty: value.New([]uint{1, 0}), |
||||
|
}, |
||||
|
"array uint64": { |
||||
|
flag: input.ValueUint64 | input.ValueArray, |
||||
|
value: value.New([]uint64{1}), |
||||
|
empty: value.New([]uint64{0}), |
||||
|
}, |
||||
|
"array float64": { |
||||
|
flag: input.ValueFloat64 | input.ValueArray, |
||||
|
value: value.New([]float64{0.2}), |
||||
|
empty: value.New([]float64{0}), |
||||
|
}, |
||||
|
"array bool": { |
||||
|
flag: input.ValueBool | input.ValueArray, |
||||
|
value: value.New([]bool{true, false}), |
||||
|
empty: value.New([]bool{}), |
||||
|
}, |
||||
|
"array duration": { |
||||
|
flag: input.ValueDuration | input.ValueArray, |
||||
|
value: value.New([]time.Duration{time.Second}), |
||||
|
empty: value.New([]time.Duration{time.Second, 0}), |
||||
|
}, |
||||
|
"array time": { |
||||
|
flag: input.ValueTime | input.ValueArray, |
||||
|
value: value.New([]time.Time{time.Now()}), |
||||
|
empty: value.New([]time.Time{{}, time.Now()}), |
||||
|
}, |
||||
|
"array string": { |
||||
|
flag: input.ValueArray, |
||||
|
value: value.New([]string{"value"}), |
||||
|
empty: value.New([]string{""}), |
||||
|
}, |
||||
|
"int": { |
||||
|
flag: input.ValueInt, |
||||
|
value: value.New(int(1)), |
||||
|
}, |
||||
|
"int64": { |
||||
|
flag: input.ValueInt64, |
||||
|
value: value.New(int64(2)), |
||||
|
}, |
||||
|
"uint": { |
||||
|
flag: input.ValueUint, |
||||
|
value: value.New(uint(1)), |
||||
|
empty: value.New([]uint{1}), |
||||
|
}, |
||||
|
"uint64": { |
||||
|
flag: input.ValueUint64, |
||||
|
value: value.New(uint64(10)), |
||||
|
}, |
||||
|
"float64": { |
||||
|
flag: input.ValueFloat64, |
||||
|
value: value.New(float64(.00001)), |
||||
|
}, |
||||
|
"duration": { |
||||
|
flag: input.ValueDuration, |
||||
|
value: value.New(time.Minute), |
||||
|
empty: value.New("same string"), |
||||
|
}, |
||||
|
"time": {flag: input.ValueTime, value: value.New(time.Now())}, |
||||
|
"string": {value: value.New("string"), empty: value.New("")}, |
||||
|
} |
||||
|
|
||||
|
for name, ca := range cases { |
||||
|
valid := validator.NotBlank(ca.flag) |
||||
|
if err := valid(ca.value); err != nil { |
||||
|
t.Errorf("case: %s, expected error <nil>, got: %s", name, err) |
||||
|
} |
||||
|
|
||||
|
if ca.empty == nil { |
||||
|
ca.empty = &value.Empty{} |
||||
|
} |
||||
|
|
||||
|
if err := valid(ca.empty); err == nil || !errors.Is(err, validator.ErrNotBlank) { |
||||
|
t.Errorf("case: %s, expect: %s, got:%s", name, validator.ErrNotBlank, err) |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,15 @@ |
|||||
|
package validator |
||||
|
|
||||
|
import "gitoa.ru/go-4devs/console/input" |
||||
|
|
||||
|
func Valid(v ...func(input.Value) error) func(input.Value) error { |
||||
|
return func(in input.Value) error { |
||||
|
for _, valid := range v { |
||||
|
if err := valid(in); err != nil { |
||||
|
return err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
} |
@ -0,0 +1,28 @@ |
|||||
|
package validator_test |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"testing" |
||||
|
|
||||
|
"gitoa.ru/go-4devs/console/input" |
||||
|
"gitoa.ru/go-4devs/console/input/value" |
||||
|
"gitoa.ru/go-4devs/console/validator" |
||||
|
) |
||||
|
|
||||
|
func TestValid(t *testing.T) { |
||||
|
validValue := value.New("one") |
||||
|
invalidValue := value.New([]string{"one"}) |
||||
|
|
||||
|
valid := validator.Valid( |
||||
|
validator.NotBlank(input.ValueString), |
||||
|
validator.Enum("one", "two"), |
||||
|
) |
||||
|
|
||||
|
if err := valid(validValue); err != nil { |
||||
|
t.Errorf("expected valid value, got: %s", err) |
||||
|
} |
||||
|
|
||||
|
if err := valid(invalidValue); !errors.Is(err, validator.ErrNotBlank) { |
||||
|
t.Errorf("expected not blank, got:%s", err) |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue