Merge pull request 'dump-reference' (#39) from dump-reference into master
All checks were successful
Go Action / goaction (push) Successful in 1m0s

Reviewed-on: #39
This commit was merged in pull request #39.
This commit is contained in:
2026-01-04 17:49:02 +03:00
6 changed files with 482 additions and 21 deletions

401
provider/arg/dump.go Normal file
View File

@@ -0,0 +1,401 @@
package arg
import (
"bytes"
"encoding"
"encoding/json"
"errors"
"fmt"
"io"
"sort"
"strings"
"time"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/param"
)
const (
defaultSpace = 2
)
func ResolveStyle(params param.Params) ViewStyle {
var vs ViewStyle
data, ok := params.Param(paramDumpReferenceView)
if ok {
vs, _ = data.(ViewStyle)
}
return vs
}
type ViewStyle struct {
Comment Style
Info Style
MLen int
}
func (v ViewStyle) ILen() int {
return v.Info.Len() + v.MLen
}
type Style struct {
Start string
End string
}
func (s Style) Len() int {
return len(s.End) + len(s.Start)
}
func NewDump() Dump {
return Dump{
sep: dash,
space: defaultSpace,
}
}
type Dump struct {
sep string
space int
}
func (d Dump) Reference(w io.Writer, opt config.Options) error {
views := NewViews(opt, nil)
style := ResolveStyle(opt)
style.MLen = d.keyMaxLen(views...)
if args := views.Arguments(); len(args) > 0 {
if aerr := d.writeArguments(w, style, args...); aerr != nil {
return fmt.Errorf("write arguments:%w", aerr)
}
}
if opts := views.Options(); len(opts) > 0 {
if oerr := d.writeOptions(w, style, opts...); oerr != nil {
return fmt.Errorf("write option:%w", oerr)
}
}
return nil
}
//nolint:mnd
func (d Dump) keyMaxLen(views ...View) int {
var maxLen int
for _, vi := range views {
vlen := len(vi.Name(d.sep)) + 6
if !vi.IsBool() {
vlen = vlen*2 + 1
}
if def := vi.Default(); def != "" {
vlen += 2
}
if vlen > maxLen {
maxLen = vlen
}
}
return maxLen
}
func (d Dump) writeArguments(w io.Writer, style ViewStyle, args ...View) error {
_, err := fmt.Fprintf(w, "\n%sArguments:%s\n",
style.Comment.Start,
style.Comment.End,
)
for _, arg := range args {
alen, ierr := fmt.Fprintf(w, "%s%s%s%s",
strings.Repeat(" ", d.space),
style.Info.Start,
arg.Name(d.sep),
style.Info.End,
)
if ierr != nil {
err = errors.Join(err, ierr)
}
_, ierr = fmt.Fprint(w, strings.Repeat(" ", style.ILen()+d.space-alen))
if ierr != nil {
err = errors.Join(err, ierr)
}
_, ierr = fmt.Fprint(w, arg.Description())
if ierr != nil {
err = errors.Join(err, ierr)
}
if def := arg.Default(); def != "" {
ierr := d.writeDefault(w, style, def)
if ierr != nil {
err = errors.Join(err, ierr)
}
}
_, ierr = fmt.Fprint(w, "\n")
if ierr != nil {
err = errors.Join(err, ierr)
}
}
if err != nil {
return fmt.Errorf("%w", err)
}
return nil
}
//nolint:gocognit,gocyclo,cyclop
func (d Dump) writeOptions(w io.Writer, style ViewStyle, opts ...View) error {
_, err := fmt.Fprintf(w, "\n%sOptions:%s\n",
style.Comment.Start,
style.Comment.End,
)
for _, opt := range opts {
if opt.IsHidden() {
continue
}
var op bytes.Buffer
_, oerr := fmt.Fprintf(&op, "%s%s", strings.Repeat(" ", d.space), style.Info.Start)
if oerr != nil {
err = errors.Join(err, oerr)
}
if short := opt.Short(); short != "" {
op.WriteString("-")
op.WriteString(short)
op.WriteString(", ")
} else {
op.WriteString(" ")
}
op.WriteString("--")
op.WriteString(opt.Name(d.sep))
if !opt.IsBool() {
if !opt.IsRequired() {
op.WriteString("[")
}
op.WriteString("=")
op.WriteString(strings.ToUpper(opt.Name(d.sep)))
if !opt.IsRequired() {
op.WriteString("]")
}
}
_, oerr = fmt.Fprintf(&op, "%s", style.Info.End)
if oerr != nil {
err = errors.Join(err, oerr)
}
olen, oerr := w.Write(op.Bytes())
if oerr != nil {
err = errors.Join(err, oerr)
}
_, oerr = fmt.Fprintf(w, "%s%s",
strings.Repeat(" ", style.ILen()+d.space-olen),
opt.Description(),
)
if oerr != nil {
err = errors.Join(err, oerr)
}
if def := opt.Default(); def != "" {
oerr = d.writeDefault(w, style, def)
if oerr != nil {
err = errors.Join(err, oerr)
}
}
if opt.IsSlice() {
_, oerr = fmt.Fprintf(w, "%s (multiple values allowed)%s", style.Comment.Start, style.Comment.End)
if oerr != nil {
err = errors.Join(err, oerr)
}
}
_, oerr = fmt.Fprint(w, "\n")
if oerr != nil {
err = errors.Join(err, oerr)
}
}
if err != nil {
return fmt.Errorf("write options:%w", err)
}
return nil
}
func (d Dump) writeDefault(w io.Writer, style ViewStyle, data string) error {
_, err := fmt.Fprintf(w, " %s[default:%s]%s",
style.Comment.Start,
data,
style.Comment.End,
)
if err != nil {
return fmt.Errorf("default:%w", err)
}
return nil
}
func NewViews(opts config.Options, parent *View) Views {
views := make(Views, 0, len(opts.Options()))
for _, opt := range opts.Options() {
views = append(views, newViews(opt, parent)...)
}
return views
}
func newViews(opt config.Option, parent *View) []View {
view := NewView(opt, parent)
switch one := opt.(type) {
case config.Group:
return NewViews(one, &view)
default:
return []View{view}
}
}
type Views []View
func (v Views) Arguments() []View {
args := make([]View, 0, len(v))
for _, view := range v {
if view.IsArgument() {
args = append(args, view)
}
}
sort.Slice(args, func(i, j int) bool {
return args[i].Pos() < args[j].Pos()
})
return args
}
func (v Views) Options() []View {
opts := make([]View, 0, len(v))
for _, view := range v {
if !view.IsArgument() {
opts = append(opts, view)
}
}
sort.Slice(opts, func(i, j int) bool {
return opts[i].Name(dash) < opts[j].Name(dash)
})
return opts
}
func NewView(params config.Option, parent *View) View {
pos, ok := ParamArgument(params)
keys := make([]string, 0)
if parent != nil {
keys = append(keys, parent.Keys()...)
}
if name := params.Name(); name != "" {
keys = append(keys, name)
}
return View{
pos: pos,
isArgument: ok,
keys: keys,
parent: parent,
Params: params,
}
}
type View struct {
param.Params
keys []string
pos uint64
isArgument bool
parent *View
}
func (v View) Name(delimiter string) string {
return strings.Join(v.keys, delimiter)
}
func (v View) IsArgument() bool {
return v.isArgument
}
func (v View) Keys() []string {
return v.keys
}
func (v View) Pos() uint64 {
return v.pos
}
func (v View) Default() string {
data, ok := option.DataDefaut(v.Params)
if !ok {
return ""
}
switch dt := data.(type) {
case time.Time:
return dt.Format(time.RFC3339)
case encoding.TextMarshaler:
if res, err := dt.MarshalText(); err == nil {
return string(res)
}
case json.Marshaler:
if res, err := dt.MarshalJSON(); err == nil {
return string(res)
}
}
return fmt.Sprintf("%v", data)
}
func (v View) Description() string {
return param.Description(v.Params)
}
func (v View) IsHidden() bool {
return option.IsHidden(v.Params)
}
func (v View) Short() string {
short, _ := option.ParamShort(v.Params)
return short
}
func (v View) IsRequired() bool {
return option.IsHidden(v.Params)
}
func (v View) IsBool() bool {
return option.IsBool(v.Params)
}
func (v View) IsSlice() bool {
return option.IsSlice(v.Params)
}

35
provider/arg/dump_test.go Normal file
View File

@@ -0,0 +1,35 @@
package arg_test
import (
"bytes"
"testing"
"gitoa.ru/go-4devs/config/provider/arg"
"gitoa.ru/go-4devs/config/test/require"
)
func TestDumpReference(t *testing.T) {
t.Parallel()
//nolint:dupword
const expect = `
Arguments:
config config [default:config.hcl]
user-name username
Options:
--end-after[=END-AFTER] after (multiple values allowed)
--end-{service}-after[=END-{SERVICE}-AFTER] after
-l, --listen[=LISTEN] listen [default:8080]
--start-at[=START-AT] start at [default:2010-01-02T15:04:05Z]
-t, --timeout[=TIMEOUT] timeout (multiple values allowed)
-u, --url[=URL] url (multiple values allowed)
-p, --user-password[=USER-PASSWORD] user pass
`
dump := arg.NewDump()
var buff bytes.Buffer
require.NoError(t, dump.Reference(&buff, testOptions(t)))
require.Equal(t, expect, buff.String())
}

View File

@@ -0,0 +1,38 @@
package arg_test
import (
"testing"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/group"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/definition/proto"
"gitoa.ru/go-4devs/config/provider/arg"
"gitoa.ru/go-4devs/config/test"
)
func testOptions(t *testing.T) config.Options {
t.Helper()
def := definition.New()
def.Add(
option.Int("listen", "listen", option.Short('l'), option.Default(8080)),
option.String("config", "config", arg.Argument, option.Default("config.hcl")),
group.New("user", "configure user",
option.String("name", "username", arg.Argument),
option.String("password", "user pass", option.Short('p')),
),
option.String("url", "url", option.Short('u'), option.Slice),
option.Duration("timeout", "timeout", option.Short('t'), option.Slice),
option.Time("start-at", "start at", option.Default(test.Time("2010-01-02T15:04:05Z"))),
group.New("end", "end at",
option.Time("after", "after", option.Slice),
proto.New("service", "service after",
option.Time("after", "after"),
),
),
)
return def
}

View File

@@ -10,6 +10,7 @@ type keyParam int
const (
paramArgument keyParam = iota + 1
paramDumpReferenceView
)
//nolint:gochecknoglobals

View File

@@ -3,6 +3,7 @@ package arg
import (
"context"
"fmt"
"io"
"os"
"strings"
@@ -115,6 +116,10 @@ func (i *Argv) Bind(ctx context.Context, def config.Variables) error {
return nil
}
func (i *Argv) DumpRefernce(_ context.Context, w io.Writer, opt config.Options) error {
return NewDump().Reference(w, opt)
}
func (i *Argv) parseLongOption(arg string, def config.Variables) error {
var value *string

View File

@@ -8,10 +8,6 @@ import (
"time"
"gitoa.ru/go-4devs/config"
"gitoa.ru/go-4devs/config/definition"
"gitoa.ru/go-4devs/config/definition/group"
"gitoa.ru/go-4devs/config/definition/option"
"gitoa.ru/go-4devs/config/definition/proto"
"gitoa.ru/go-4devs/config/provider/arg"
"gitoa.ru/go-4devs/config/test"
"gitoa.ru/go-4devs/config/test/require"
@@ -52,7 +48,7 @@ func TestProviderBind(t *testing.T) {
t.Parallel()
args := []string{
"-p 8080",
"-l 8080",
"--config=config.hcl",
"-u http://4devs.io",
"--url=https://4devs.io",
@@ -84,22 +80,7 @@ func TestProviderBind(t *testing.T) {
func testVariables(t *testing.T) config.Variables {
t.Helper()
def := definition.New()
def.Add(
option.Int("listen", "listen", option.Short('p')),
option.String("config", "config"),
option.String("url", "url", option.Short('u'), option.Slice),
option.Duration("timeout", "timeout", option.Short('t'), option.Slice),
option.Time("start-at", "start at"),
group.New("end", "end at",
option.Time("after", "after", option.Slice),
proto.New("service", "service after",
option.Time("after", "after"),
),
),
)
return config.NewVars(def.Options()...)
return config.NewVars(testOptions(t).Options()...)
}
type Duration struct {