Skip to content
Snippets Groups Projects
Verified Commit 5004bcd2 authored by Volker Schukai's avatar Volker Schukai :alien:
Browse files

feat: initial version

parent b3c2b895
No related branches found
No related tags found
No related merge requests found
{{ range .Versions }}
<a name="{{ .Tag.Name }}"></a>
## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }}
{{ range .CommitGroups -}}
### {{ .Title }}
{{ range .Commits -}}
- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}
{{ end }}
{{ end -}}
{{- if .NoteGroups -}}
{{ range .NoteGroups -}}
### {{ .Title }}
{{ range .Notes }}
{{ .Body }}
{{ end }}
{{ end -}}
{{ end -}}
{{ end -}}
{{- if .Versions }}
{{ range .Versions -}}
{{ if .Tag.Previous -}}
[{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}
{{ end -}}
{{ end -}}
{{ end -}}
\ No newline at end of file
style: gitlab
template: CHANGELOG.tpl.md
info:
title: CHANGELOG
repository_url: https://gitlab.schukai.com/oss/libraries/go/application/configuration
options:
commits:
filters:
Type:
- feat
- fix
- doc
- refactor
- perf
- test
- chore
## deprecated types and typos
- docs
- documentation
- feat
- added
- add
- bugfix
- revert
- update
- updates
- change
- changed
commit_groups:
title_maps:
feat: Add Features
fix: Bug Fixes
doc: Documentation
refactor: Code Refactoring
perf: Performance Improvements
test: Tests
## Chore is used for all other changes that don't fit in the other categories
chore: Changes
## deprecated types and typos
docs: Documentation
documentation: Documentation
added: Add Features
add: Add Features
bugfix: Bug Fixes
revert: Reverts
update: Changes
updates: Changes
change: Changes
changed: Changes
header:
pattern: "^((\\w+)\\s.*)$"
pattern_maps:
- Subject
- Type
notes:
keywords:
- BREAKING CHANGE
LICENSE 0 → 100644
This diff is collapsed.
...@@ -62,7 +62,14 @@ ifndef VERSION_BIN ...@@ -62,7 +62,14 @@ ifndef VERSION_BIN
$(shell chmod +x $(VERSION_BIN_PATH)) $(shell chmod +x $(VERSION_BIN_PATH))
endif endif
GIT_CHGLOG_BIN := $(shell command -v git-chglog 2> /dev/null)
ifeq ($(GIT_CHGLOG_BIN),)
$(shell go install github.com/git-chglog/git-chglog/cmd/git-chglog@latest)
endif
RELEASE_FILE ?= $(PROJECT_ROOT)release.json RELEASE_FILE ?= $(PROJECT_ROOT)release.json
CHANGELOG_FILE ?= $(PROJECT_ROOT)CHANGELOG.md
ifeq ("$(wildcard $(RELEASE_FILE))","") ifeq ("$(wildcard $(RELEASE_FILE))","")
$(shell echo '{"version":"0.1.0"}' > $(RELEASE_FILE)) $(shell echo '{"version":"0.1.0"}' > $(RELEASE_FILE))
...@@ -72,21 +79,18 @@ PROJECT_VERSION ?= $(shell cat $(RELEASE_FILE) | jq -r .version) ...@@ -72,21 +79,18 @@ PROJECT_VERSION ?= $(shell cat $(RELEASE_FILE) | jq -r .version)
PROJECT_BUILD_DATE ?= $(shell $(VERSION_BIN) date) PROJECT_BUILD_DATE ?= $(shell $(VERSION_BIN) date)
.PHONY: next-patch-version .PHONY: next-patch-version
## create next patch version
next-patch-version: check-clean-repo next-patch-version: check-clean-repo
echo "Creating next version" echo "Creating next version"
$(VERSION_BIN) patch --path $(RELEASE_FILE) --selector "version" $(VERSION_BIN) patch --path $(RELEASE_FILE) --selector "version"
git add $(RELEASE_FILE) && git commit -m "Bump version to $$(cat $(RELEASE_FILE) | jq -r .version)" git add $(RELEASE_FILE) && git commit -m "Bump version to $$(cat $(RELEASE_FILE) | jq -r .version)"
.PHONY: next-minor-version .PHONY: next-minor-version
## create next minor version
next-minor-version: check-clean-repo next-minor-version: check-clean-repo
echo "Creating next minor version" echo "Creating next minor version"
$(VERSION_BIN) minor --path $(RELEASE_FILE) --selector "version" $(VERSION_BIN) minor --path $(RELEASE_FILE) --selector "version"
git add $(RELEASE_FILE) && git commit -m "Bump version to $$( cat $(RELEASE_FILE) | jq -r .version)" git add $(RELEASE_FILE) && git commit -m "Bump version to $$( cat $(RELEASE_FILE) | jq -r .version)"
.PHONY: next-major-version .PHONY: next-major-version
## create next major version
next-major-version: check-clean-repo next-major-version: check-clean-repo
echo "Creating next minor version" echo "Creating next minor version"
$(VERSION_BIN) major --path $(RELEASE_FILE) --selector "version" $(VERSION_BIN) major --path $(RELEASE_FILE) --selector "version"
...@@ -100,18 +104,24 @@ check-clean-repo: ...@@ -100,18 +104,24 @@ check-clean-repo:
tag-patch-version: next-patch-version tag-patch-version: next-patch-version
echo "Tagging patch version" echo "Tagging patch version"
$(eval PROJECT_VERSION := $(shell cat $(RELEASE_FILE) | jq -r .version)) $(eval PROJECT_VERSION := $(shell cat $(RELEASE_FILE) | jq -r .version))
git-chglog --next-tag v$(PROJECT_VERSION) -o $(CHANGELOG_FILE)
git add $(CHANGELOG_FILE) && git commit -m "Update changelog"
git tag -a v$(PROJECT_VERSION) -m "Version $(PROJECT_VERSION)" git tag -a v$(PROJECT_VERSION) -m "Version $(PROJECT_VERSION)"
## tag repository with next minor version ## tag repository with next minor version
tag-minor-version: next-minor-version tag-minor-version: next-minor-version
echo "Tagging minor version" echo "Tagging minor version"
$(eval PROJECT_VERSION := $(shell cat $(RELEASE_FILE) | jq -r .version)) $(eval PROJECT_VERSION := $(shell cat $(RELEASE_FILE) | jq -r .version))
git-chglog --next-tag v$(PROJECT_VERSION) -o $(CHANGELOG_FILE)
git add $(CHANGELOG_FILE) && git commit -m "Update changelog"
git tag -a v$(PROJECT_VERSION) -m "Version $(PROJECT_VERSION)" git tag -a v$(PROJECT_VERSION) -m "Version $(PROJECT_VERSION)"
## tag repository with next major version ## tag repository with next major version
tag-major-version: next-major-version tag-major-version: next-major-version
echo "Tagging major version" echo "Tagging major version"
$(eval PROJECT_VERSION := $(shell cat $(RELEASE_FILE) | jq -r .version)) $(eval PROJECT_VERSION := $(shell cat $(RELEASE_FILE) | jq -r .version))
git-chglog --next-tag v$(PROJECT_VERSION) -o $(CHANGELOG_FILE)
git add $(CHANGELOG_FILE) && git commit -m "Update changelog"
git tag -a v$(PROJECT_VERSION) -m "Version $(PROJECT_VERSION)" git tag -a v$(PROJECT_VERSION) -m "Version $(PROJECT_VERSION)"
GO_MOD_FILE := $(SOURCE_PATH)go.mod GO_MOD_FILE := $(SOURCE_PATH)go.mod
......
...@@ -7,7 +7,11 @@ to be able to define and use a structure with flags. ...@@ -7,7 +7,11 @@ to be able to define and use a structure with flags.
It supports: It supports:
* [x] Define flags in a structure * [X] Define flags in a structure
* [X] Define callbacks for flags
* [X] Define default values for flags
* [X] Define aliases for flags
* [X] Define required flags
## Installation ## Installation
...@@ -19,18 +23,156 @@ go get gitlab.schukai.com/oss/libraries/go/network/xflags ...@@ -19,18 +23,156 @@ go get gitlab.schukai.com/oss/libraries/go/network/xflags
## Usage ## Usage
## Change Log ### Initialize
### 1.0.0 A new flag set is created using the `xflags.New()` function. The passed
structure is used type for the flags.
* Initial release ```go
package main
import (
"fmt"
"os"
"gitlab.schukai.com/oss/libraries/go/network/xflags"
)
```
### Definition
The flags are defined in the structure. The structure can be nested.
The name of the field is used as the name of the flag. The type of the
field is used as the type of the flag.
```go
type Definition struct {
Verbose bool `short:"v" long:"verbose" description:"Show verbose debug information"`
Serve struct {
Host string `short:"h" long:"host" description:"Host to bind to" default:"localhost"`
Port int `short:"p" long:"port" description:"Port to bind to" default:"8080"`
} `command:"serve" description:"Run the HTTP server" call:"DoServe"`
}
```
The following tags are supported:
| Tag | Context | Description |
|---------------|---------|--------------------------------------------|
| `short` | Value | Short name of the flag. |
| `long` | Value | Long name of the flag. |
| `description` | Value | Description of the flag. |
| `required` | Value | Flag is required. |
| `command` | Command | Flag is a command. |
| `call` | Command | Function to call when the command is used. |
| `ignore` | -/- | Property is ignored. |
### Callbacks
The functions are called up with a receiver. The receiver is the
configuration. The function must have the following signature:
`func (d *Definition) DoServe(s *setting[Definition])`
Let's assume we have the above definition. The Property `Serve` contains
the command `serve`. Furthermore, the command has the tag `call` with
the value `DoServe`. The function `DoServe` is called when the command
`serve` is used.
The function is called with the receiver `*Definition`
An example for the function `DoServe`:
```go
func (d *Definition) DoServe(_ *setting[Definition]) {
fmt.Printf("Serving on %s:%d", d.Serve.Host, d.Serve.Port)
}
```
In this example, the function is called with the receiver `*Definition`.
The function is called with the setting `*setting[Definition]`. The
setting is used to get the values of the flags. But in this example, we
don't need the setting. So we use the underscore `_` to ignore the
setting.
### New Setting
The function `New` creates a new setting for the given
definition. The function returns a pointer to the setting.
The first argument is a name for the setting. The second argument is the
definition.
A good choice for the name is the argument `os.Args[0]`.
```go
setting := New(os.Args[0], Definition{})
```
### Parse
The flags are parsed using the `Parse()` function. The function returns
the command and the setting. The command is the name of the command
which was used. The setting is the setting of the flags.
```go
setting.Parse(os.Args[1:])
```
For testing, you can use the following arguments:
```go
setting.Parse([]string{"--verbose", "serve", "--host", "localhost", "--port", "8080"})
```
### Get Values
The values of the flags are available in the setting. The values are
available in the structure. The structure is the same as the definition.
```go
fmt.Printf("Host: %s", setting.GetValues().Serve.Host)
fmt.Printf("Port: %d", setting.GetValues().Serve.Port)
```
### Execute
The function `Execute()` executes the command. See the section
[Callbacks](#callbacks) for more information.
```go
setting.Execute()
```
## Contributing ## Contributing
Merge requests are welcome. For major changes, please open an issue first to discuss what Merge requests are welcome. For major changes, please open an issue first to discuss what
you would like to change. you would like to change. **Please make sure to update tests as appropriate.**
Versioning is done with [SemVer](https://semver.org/).
Changelog is generated with [git-chglog](https://github.com/git-chglog/git-chglog#git-chglog)
Commit messages should follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
Messages are started with a type, which is one of the following:
Please make sure to update tests as appropriate. - **feat**: A new feature
- **fix**: A bug fix
- **doc**: Documentation only changes
- **refactor**: A code change that neither fixes a bug nor adds a feature
- **perf**: A code change that improves performance
- **test**: Adding missing or correcting existing tests
- **chore**: Other changes that don't modify src or test files
The footer would be used for a reference to an issue or a breaking change.
A commit that has a footer `BREAKING CHANGE:`, or appends a ! after the type/scope,
introduces a breaking API change (correlating with MAJOR in semantic versioning).
A BREAKING CHANGE can be part of commits of any type.
the following is an example of a commit message:
```text
feat: add 'extras' field
```
## License ## License
......
api.go 0 → 100644
package xflags
import (
"bytes"
"flag"
"io"
"reflect"
)
// New creates a new instance of the settings.
// name should be the name of the command and comes from the first argument of the command line.
// os.Args[0] is a good choice.
func New[C any](name string, definitions C) *setting[C] {
s := &setting[C]{
config: config{
errorHandling: flag.ContinueOnError,
},
}
if reflect.TypeOf(definitions).Kind() != reflect.Struct {
s.errors = append(s.errors, newUnsupportedReflectKindError(reflect.TypeOf(definitions)))
return s
}
buf := bytes.NewBufferString("")
s.flagOutput = io.Writer(buf)
s.definitions = definitions
s.initCommands(name)
return s
}
// FlagOutput returns the writer where the flag package writes its output.
func (s *setting[C]) FlagOutput() string {
return s.flagOutput.(*bytes.Buffer).String()
}
// Args Returns not parsed arguments.
func (s *setting[C]) Args() []string {
return s.args
}
package xflags
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestNewIntWithError(t *testing.T) {
// two is not a struct
commands := New("root", 2)
assert.True(t, commands.HasErrors())
}
func TestNew(t *testing.T) {
c := struct {
Verbose bool `short:"v" long:"verbose" description:"Show verbose debug information"`
}{
Verbose: false,
}
commands := New("root", c)
assert.False(t, commands.HasErrors())
}
type CmdTest1 struct {
A bool `short:"a"`
Sub1 struct {
B bool `short:"b"`
Sub2 struct {
C bool `short:"c"`
Sub3 struct {
D bool `short:"d"`
} `command:"sub3"`
} `command:"sub2"`
} `command:"sub1"`
aa int `short:"x"`
}
func TestCommand2(t *testing.T) {
commands := New("root", CmdTest1{})
args := []string{"root", "-a", "sub1", "-b", "sub2", "-c", "sub3", "-d"}
commands.Parse(args)
assert.False(t, commands.HasErrors())
if commands.HasErrors() {
t.Log(commands.Errors())
}
assert.True(t, CmdTest1(commands.GetValues()).A)
assert.True(t, CmdTest1(commands.GetValues()).Sub1.B)
assert.True(t, CmdTest1(commands.GetValues()).Sub1.Sub2.C)
assert.True(t, CmdTest1(commands.GetValues()).Sub1.Sub2.Sub3.D)
assert.False(t, commands.HasErrors())
}
package xflags
import (
"flag"
"reflect"
)
const (
tagIgnore = "ignore"
tagCall = "call"
tagCommand = "command"
tagShort = "short"
tagLong = "long"
tagDescription = "description"
)
type cmd[C any] struct {
name string
flagSet *flag.FlagSet
mapping map[string]string
commands []*cmd[C]
settings *setting[C]
valuePath []string
functionName string
}
func (c *cmd[C]) parse(args []string) {
s := c.settings
for _, command := range c.commands {
if command.name == args[0] {
if command.flagSet == nil {
s.errors = append(s.errors, newMissingFlagSetError(command.name))
continue
}
err := command.flagSet.Parse(args[1:])
if err != nil {
s.errors = append(s.errors, err)
continue
}
s.assignValues(*command)
r := command.flagSet.Args()
if len(r) == 0 {
c.settings.args = []string{}
continue
}
c.settings.args = r
command.parse(r)
}
}
}
func buildCommandStruct[C any](s *setting[C], name, fkt string, errorHandling flag.ErrorHandling, path []string) *cmd[C] {
cc := &cmd[C]{
name: name,
flagSet: flag.NewFlagSet(name, errorHandling),
commands: []*cmd[C]{},
settings: s,
mapping: map[string]string{},
valuePath: path,
functionName: fkt,
}
cc.flagSet.SetOutput(s.flagOutput)
return cc
}
func (c *cmd[C]) initCommands(x reflect.Value, m map[string]string, path string) {
if x.Kind() != reflect.Struct {
c.settings.errors = append(c.settings.errors, newUnsupportedReflectKindError(x.Type()))
return
}
cc := buildCommandStruct[C](c.settings, m[tagCommand], m[tagCall], c.settings.config.errorHandling, append(c.valuePath, path))
cc.parseStruct(x.Interface())
c.commands = append(c.commands, cc)
}
func (c *cmd[C]) initFlags(x reflect.Value, m map[string]string) {
if x.Kind() == reflect.Struct {
c.settings.errors = append(c.settings.errors, newUnsupportedReflectKindError(x.Type()))
return
}
switch x.Kind() {
case reflect.Bool:
if m[tagShort] != "" {
c.flagSet.Bool(m[tagShort], x.Bool(), m[tagDescription])
}
if m[tagLong] != "" {
c.flagSet.Bool(m[tagLong], x.Bool(), m[tagDescription])
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if m[tagShort] != "" {
c.flagSet.Int(m[tagShort], int(x.Int()), m[tagDescription])
}
if m[tagLong] != "" {
c.flagSet.Int(m[tagLong], int(x.Int()), m[tagDescription])
}
case reflect.Float32, reflect.Float64:
if m[tagShort] != "" {
c.flagSet.Float64(m[tagShort], x.Float(), m[tagDescription])
}
if m[tagLong] != "" {
c.flagSet.Float64(m[tagLong], x.Float(), m[tagDescription])
}
case reflect.String:
if m[tagShort] != "" {
c.flagSet.String(m[tagShort], x.String(), m[tagDescription])
}
if m[tagLong] != "" {
c.flagSet.String(m[tagLong], x.String(), m[tagDescription])
}
default:
c.settings.errors = append(c.settings.errors, newUnsupportedFlagTypeError(x.Type()))
}
}
func (c *cmd[C]) parseStruct(dta any) {
t := reflect.TypeOf(dta)
if t.Kind() != reflect.Struct {
c.settings.errors = append(c.settings.errors, newUnsupportedReflectKindError(t))
return
}
v := reflect.ValueOf(dta)
for i := 0; i < v.NumField(); i++ {
x := v.Field(i)
m := getTagMap(v.Type().Field(i))
if m[tagShort] != "" || m[tagLong] != "" {
if m[tagCommand] != "" {
c.settings.errors = append(c.settings.errors, newAmbiguousTagError(v.Type().Field(i).Name, m))
continue
}
if m[tagShort] != "" {
c.mapping[m[tagShort]] = v.Type().Field(i).Name
}
if m[tagLong] != "" {
c.mapping[m[tagLong]] = v.Type().Field(i).Name
}
c.initFlags(x, m)
} else if m[tagCommand] != "" {
//c.valuePath = append(c.valuePath, )
c.mapping[m[tagCommand]] = v.Type().Field(i).Name
c.initCommands(x, m, v.Type().Field(i).Name)
} else if m[tagIgnore] != "" {
continue
} else {
c.settings.errors = append(c.settings.errors, newMissingTagError(v.Type().Field(i).Name))
}
}
}
package xflags
import (
"flag"
"github.com/stretchr/testify/assert"
"testing"
)
type testStructParseCommand1 struct {
V1 bool `short:"v1"`
Cmd1 struct {
V2 bool `short:"v2"`
V4 int `short:"v4"`
} `command:"cmd1"`
Cmd2 struct {
V3 bool `short:"v3"`
Cmd3 struct {
V3 bool `short:"v3"`
} `command:"cmd3"`
} `command:"cmd2"`
}
func TestParse(t *testing.T) {
tests := []struct {
commandline []string
}{
{[]string{"command1"}},
}
for _, test := range tests {
t.Run(test.commandline[0], func(t *testing.T) {
c := &cmd[testStructParseCommand1]{}
c.settings = &setting[testStructParseCommand1]{}
c.commands = []*cmd[testStructParseCommand1]{}
c.commands = append(c.commands, &cmd[testStructParseCommand1]{
name: "command1",
flagSet: &flag.FlagSet{},
})
c.parse(test.commandline)
assert.Equal(t, 0, len(c.settings.errors))
})
}
}
doc.go 0 → 100644
// Copyright schukai GmbH. All rights reserved.
// Use of this source code is governed by a GNU AGPLv3
// license that can be found in the LICENSE file.
// This package provides a simple way to create a CLI application
// with subcommands.
package xflags
error.go 0 → 100644
package xflags
import (
"errors"
"reflect"
)
// ResetError is used to reset the error to nil
// After calling this function, the call HasErrors() will return false
func (s *setting[C]) ResetErrors() *setting[C] {
s.errors = []error{}
return s
}
// Check if the setting contains errors
func (s *setting[C]) HasErrors() bool {
return len(s.errors) > 0
}
// Get all errors
func (s *setting[C]) Errors() []error {
return s.errors
}
var WatchListNotInitializedError = errors.New("watch list not initialized")
var MissingCommandError = errors.New("missing command")
var NotParsedError = errors.New("flag set not parsed")
// At the reflect level, some types are not supported
type UnsupportedReflectKindError error
func newUnsupportedReflectKindError(t reflect.Type) UnsupportedReflectKindError {
return UnsupportedReflectKindError(errors.New("type " + t.String() + " is not supported"))
}
// AmbiguousTagError is used when a tag is ambiguous
type AmbiguousTagError error
func newAmbiguousTagError(name string, m map[string]string) AmbiguousTagError {
msg := "ambiguous tag for field " + name + ": "
for k, v := range m {
msg += k + " = " + v + ", "
}
return AmbiguousTagError(errors.New(msg))
}
// UnsupportedFlagTypeError is used when a flag type is not supported
type UnsupportedFlagTypeError error
func newUnsupportedFlagTypeError(t reflect.Type) UnsupportedFlagTypeError {
return UnsupportedFlagTypeError(errors.New("type " + t.String() + " is not supported"))
}
// EmptyTagError is used when a tag is empty
type EmptyTagError error
func newEmptyTagError(tag, name string) EmptyTagError {
return EmptyTagError(errors.New("tag " + tag + " is empty for field " + name))
}
// MissingTagError is used when a tag is missing
type MissingTagError error
func newMissingTagError(tag string) MissingTagError {
return MissingTagError(errors.New("tag " + tag + " is empty"))
}
type InvalidPathError error
func newInvalidPathError(path string) InvalidPathError {
return InvalidPathError(errors.New("invalid path " + path))
}
type UnsupportedTypeAtTopOfPathError error
func newUnsupportedTypeAtTopOfPathError(path string, t reflect.Type) UnsupportedTypeAtTopOfPathError {
return UnsupportedTypeAtTopOfPathError(errors.New("unsupported type " + t.String() + " at top of path " + path))
}
type UnsupportedTypePathError error
func newUnsupportedTypePathError(path string, t reflect.Type) UnsupportedTypeAtTopOfPathError {
return UnsupportedTypePathError(errors.New("unsupported type " + t.String() + " at path " + path))
}
type UnknownFlagError error
func newUnknownFlagError(name string) UnknownFlagError {
return UnknownFlagError(errors.New("unknown flag " + name))
}
type CannotSetError error
func newCannotSetError(name string) CannotSetError {
return CannotSetError(errors.New("cannot set " + name))
}
type MissingFlagSetError error
func newMissingFlagSetError(name string) MissingFlagSetError {
return MissingFlagSetError(errors.New("missing flag set for command " + name))
}
type StdoutError error
func newStdoutError(message string) StdoutError {
return StdoutError(errors.New(message))
}
package xflags
import (
"github.com/stretchr/testify/assert"
"reflect"
"testing"
)
func TestHasErrors(t *testing.T) {
c := New("root", 2)
assert.True(t, c.HasErrors())
c.ResetErrors()
assert.False(t, c.HasErrors())
}
func TestResetErrors(t *testing.T) {
c := New("root", 4)
c.errors = append(c.errors, newUnsupportedReflectKindError(reflect.TypeOf(1)))
assert.True(t, c.HasErrors())
c.ResetErrors()
assert.False(t, c.HasErrors())
}
func TestNewUnsupportedReflectKindError(t *testing.T) {
ref := reflect.TypeOf(1)
err := newUnsupportedReflectKindError(ref)
_, ok := err.(UnsupportedReflectKindError)
assert.True(t, ok)
}
func TestNewAmbiguousTagError(t *testing.T) {
err := newAmbiguousTagError("test", map[string]string{"short": "s", "long": "long"})
_, ok := err.(AmbiguousTagError)
assert.True(t, ok)
}
func TestNewUnsupportedFlagTypeError(t *testing.T) {
err := newUnsupportedFlagTypeError(reflect.TypeOf(make(map[string]string)))
_, ok := err.(UnsupportedFlagTypeError)
assert.True(t, ok)
}
func TestNewEmptyTagError(t *testing.T) {
err := newEmptyTagError("test", "test")
_, ok := err.(EmptyTagError)
assert.True(t, ok)
}
func TestMissingTagError(t *testing.T) {
err := newMissingTagError("test")
_, ok := err.(MissingTagError)
assert.True(t, ok)
}
func TestInvalidPathError(t *testing.T) {
err := newInvalidPathError("test")
_, ok := err.(InvalidPathError)
assert.True(t, ok)
}
func TestUnsupportedTypeAtTopOfPathError(t *testing.T) {
err := newUnsupportedTypeAtTopOfPathError("test", reflect.TypeOf(1))
_, ok := err.(UnsupportedTypeAtTopOfPathError)
assert.True(t, ok)
}
func TestUnsupportedTypePathError(t *testing.T) {
err := newUnsupportedTypeAtTopOfPathError("test", reflect.TypeOf(1))
_, ok := err.(UnsupportedTypeAtTopOfPathError)
assert.True(t, ok)
}
func TestUnknownFlagError(t *testing.T) {
err := newUnknownFlagError("test")
_, ok := err.(UnknownFlagError)
assert.True(t, ok)
}
func TestCannotSetError(t *testing.T) {
err := newCannotSetError("test")
_, ok := err.(CannotSetError)
assert.True(t, ok)
}
func TestStdoutError(t *testing.T) {
err := newStdoutError("test")
_, ok := err.(StdoutError)
assert.True(t, ok)
}
package xflags
import (
"reflect"
)
func (s *setting[C]) Execute() *setting[C] {
if len(s.errors) > 0 {
return s
}
if s.command == nil {
s.errors = append(s.errors, MissingCommandError)
return s
}
if !s.command.flagSet.Parsed() {
s.errors = append(s.errors, NotParsedError)
} else {
callCmdFunctions(s.command.commands)
}
return s
}
func callCmdFunctions[C any](commands []*cmd[C]) {
for _, command := range commands {
if command.flagSet.Parsed() {
f := reflect.ValueOf(&command.settings.definitions).MethodByName(command.functionName)
if f.IsValid() {
m := command.settings
in := []reflect.Value{reflect.ValueOf(m)}
f.Call(in)
}
callCmdFunctions(command.commands)
}
}
}
package xflags
import (
"github.com/stretchr/testify/assert"
"strconv"
"testing"
)
type testExecutionStruct struct {
callbackCounter int `ignore:"true"`
Global1 bool `short:"a" long:"global1" description:"Global 1"`
Global2 bool `short:"b" long:"global2" description:"Global 2"`
Command1 struct {
Command1Flag1 bool `short:"c" long:"command1flag1" description:"Command 1 Flag 1"`
Command1Flag2 bool `short:"d" long:"command1flag2" description:"Command 1 Flag 2"`
Command2 struct {
Command2Flag1 bool `short:"e" long:"command2flag1" description:"Command 2 Flag 1"`
Command2Flag2 bool `short:"f" long:"command2flag2" description:"Command 2 Flag 2"`
} `command:"command2" description:"Command 2" call:"Command2Callback" `
} `command:"command1" description:"Command 1" call:"Command1Callback" `
Command3 struct {
Command3Flag1 bool `short:"g" long:"command3flag1" description:"Command 3 Flag 1"`
} `command:"command3" description:"Command 3" call:"Command3Callback" `
}
func (c *testExecutionStruct) Command1Callback(s *setting[testExecutionStruct]) {
s.definitions.callbackCounter++
}
func (c *testExecutionStruct) Command2Callback(s *setting[testExecutionStruct]) {
s.definitions.callbackCounter++
}
func (c *testExecutionStruct) Command3Callback(s *setting[testExecutionStruct]) {
s.definitions.callbackCounter++
}
type testExecutionTestCases[C any] struct {
name string
args []string
targetValue int
}
func TestExec(t *testing.T) {
tests := []testExecutionTestCases[testExecutionStruct]{
{
name: "test",
args: []string{"test", "command1", "-c"},
targetValue: 1,
},
{
name: "test",
args: []string{"test", "command1", "command2", "-e"},
targetValue: 2,
}, {
name: "test",
args: []string{"test", "-a", "command3"},
targetValue: 1,
},
}
for _, tt := range tests {
t.Run(tt.args[0], func(t *testing.T) {
settings := New(tt.name, testExecutionStruct{})
assert.NotNil(t, settings)
if settings.definitions.callbackCounter != 0 {
t.Error("Callback counter should be 0")
}
settings.Parse(tt.args)
settings.Execute()
if settings.HasErrors() {
t.Error("Should not have errors")
t.Log(settings.Errors())
}
if settings.definitions.callbackCounter != tt.targetValue {
t.Error("Callback counter should be " + strconv.Itoa(tt.targetValue) + " but is " + strconv.Itoa(settings.definitions.callbackCounter))
}
//tt.test(s)
})
}
}
flag.go 0 → 100644
package xflags
import (
"flag"
"strings"
)
// Parse parses the command line arguments and assigns the values to the settings.
func (s *setting[C]) Parse(args []string) *setting[C] {
if len(s.errors) > 0 {
return s
}
if s.command == nil {
s.errors = append(s.errors, MissingCommandError)
return s
}
err := s.command.flagSet.Parse(args[1:])
if err != nil {
s.errors = append(s.errors, err)
return s
}
s.assignValues(*s.command)
r := s.command.flagSet.Args()
if len(r) == 0 {
return s
}
s.command.parse(r)
return s
}
func (s *setting[C]) assignValues(c cmd[C]) {
flgs := c.flagSet
flgs.Visit(func(f *flag.Flag) {
name := f.Name
value := f.Value.String()
k, ok := c.mapping[name]
if !ok {
s.errors = append(s.errors, newUnknownFlagError(name))
return
}
pa := append(c.valuePath, k)
p := strings.Join(pa, ".")
err := setTheValueOverPath(&s.definitions, p, value)
if err != nil {
s.errors = append(s.errors, err)
}
return
})
}
package xflags
import (
"fmt"
"github.com/stretchr/testify/assert"
"testing"
)
func TestWrongDefinitionType(t *testing.T) {
c := New("root", 2)
c.Parse([]string{"test"})
c.Execute()
assert.True(t, c.HasErrors())
}
type testExecuteCommandStruct struct {
Command1 struct {
} `command:"command1" description:"Command 1" callback:"command1Callback" `
Command2 struct {
Command3 struct {
} `command:"command3" description:"Command 3" callback:"command3Callback" `
} `command:"command2" description:"Command 2" callback:"command2Callback" `
}
func (c *testExecuteCommandStruct) command1Callback(args []string) {
fmt.Println("command1Callback", args)
}
func TestExecute1(t *testing.T) {
c := New("root", testExecuteCommandStruct{})
c.Parse([]string{"root", "command2", "command3", "commandX"})
c.Execute()
assert.False(t, c.HasErrors())
}
package xflags
import (
"github.com/stretchr/testify/assert"
"testing"
)
type testInterfaceCallbacks func(s *setting[testIntegrationStruct])
type testIntegrationStruct struct {
Help bool `short:"h" long:"help" description:"Show this help message"`
Verbose bool `short:"v" long:"verbose" description:"Enable verbose logging"`
}
type testIntegrationTestCases[C any] struct {
name string
args []string
test testInterfaceCallbacks
errors []string
}
func TestIntegrationError(t *testing.T) {
tests := []testIntegrationTestCases[testIntegrationStruct]{
{
name: "test",
args: []string{"test2", "-a"},
errors: []string{
"flag provided but not defined: -a",
},
},
}
for _, tt := range tests {
t.Run(tt.args[0], func(t *testing.T) {
settings := New(tt.name, testIntegrationStruct{})
assert.NotNil(t, settings)
s := settings.Parse(tt.args)
if !s.HasErrors() {
t.Error(s.Errors())
}
summarizedErrors := []string{}
for _, e := range s.Errors() {
summarizedErrors = append(summarizedErrors, e.Error())
}
for _, err := range tt.errors {
assert.Contains(t, summarizedErrors, err)
}
})
}
}
func TestIntegration(t *testing.T) {
tests := []testIntegrationTestCases[testIntegrationStruct]{
{
name: "test",
args: []string{"test"},
test: func(s *setting[testIntegrationStruct]) {
v := s.GetValues()
assert.NotNil(t, v)
assert.IsType(t, testIntegrationStruct{}, v)
assert.False(t, v.Verbose)
assert.False(t, s.GetValues().Verbose)
},
},
{
name: "test",
args: []string{"test", "-v"},
test: func(s *setting[testIntegrationStruct]) {
v := s.GetValues()
assert.NotNil(t, v)
assert.IsType(t, testIntegrationStruct{}, v)
assert.True(t, v.Verbose)
assert.True(t, s.GetValues().Verbose)
},
},
{
name: "test",
args: []string{"test", "--verbose"},
test: func(s *setting[testIntegrationStruct]) {
assert.True(t, s.GetValues().Verbose)
},
},
{
name: "test",
args: []string{"test", "-verbose"},
test: func(s *setting[testIntegrationStruct]) {
assert.True(t, s.GetValues().Verbose)
},
},
{
name: "test",
args: []string{"test", "sub1"},
test: func(s *setting[testIntegrationStruct]) {
assert.False(t, s.GetValues().Verbose)
},
},
}
for _, tt := range tests {
t.Run(tt.args[0], func(t *testing.T) {
settings := New(tt.name, testIntegrationStruct{})
assert.NotNil(t, settings)
s := settings.Parse(tt.args)
if s.HasErrors() {
t.Error(s.Errors())
}
assert.Equal(t, settings, s)
tt.test(settings)
})
}
}
package xflags
import (
"reflect"
"strconv"
"strings"
)
// This function returns the value of a field in a struct, given a path to the field.
func getValueFrom[D any](obj D, keyWithDots string) (interface{}, error) {
keySlice := strings.Split(keyWithDots, ".")
v := reflect.ValueOf(obj)
for _, key := range keySlice[0 : len(keySlice)-1] {
for v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return nil, newUnsupportedTypePathError(keyWithDots, v.Type())
}
v = v.FieldByName(key)
}
for v.Kind() == reflect.Ptr {
v = v.Elem()
}
// non-supporter type at the top of the path
if v.Kind() != reflect.Struct {
return nil, newUnsupportedTypeAtTopOfPathError(keyWithDots, v.Type())
}
v = v.FieldByName(keySlice[len(keySlice)-1])
if !v.IsValid() {
return nil, newInvalidPathError(keyWithDots)
}
return v.Interface(), nil
}
// This function sets the value of a field in a struct, given a path to the field.
func setTheValueOverPath[D any](obj D, keyWithDots string, newValue string) error {
keySlice := strings.Split(keyWithDots, ".")
v := reflect.ValueOf(obj)
for _, key := range keySlice[0 : len(keySlice)-1] {
for v.Kind() != reflect.Ptr {
v = v.Addr()
}
if v.Kind() != reflect.Ptr {
return newUnsupportedTypePathError(keyWithDots, v.Type())
}
elem := v.Elem()
if elem.Kind() != reflect.Struct {
return newUnsupportedTypePathError(keyWithDots, v.Type())
}
v = elem.FieldByName(key)
}
for v.Kind() == reflect.Ptr {
v = v.Elem()
}
// non-supporter type at the top of the path
if v.Kind() != reflect.Struct {
return newUnsupportedTypeAtTopOfPathError(keyWithDots, v.Type())
}
v = v.FieldByName(keySlice[len(keySlice)-1])
if !v.IsValid() {
return newInvalidPathError(keyWithDots)
}
if !v.CanSet() {
return newCannotSetError(keyWithDots)
}
switch v.Kind() {
case reflect.String:
v.SetString(newValue)
case reflect.Int:
s, err := strconv.Atoi(newValue)
if err != nil {
return err
}
v.SetInt(int64(s))
case reflect.Bool:
v.SetBool(newValue == "true")
case reflect.Float64:
s, err := strconv.ParseFloat(newValue, 64)
if err != nil {
return err
}
v.SetFloat(s)
default:
return newUnsupportedTypeAtTopOfPathError(keyWithDots, v.Type())
}
return nil
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment