diff options
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/lulu/bool.go | 39 | ||||
| -rw-r--r-- | cmd/lulu/duration.go | 29 | ||||
| -rw-r--r-- | cmd/lulu/flag.go | 209 | ||||
| -rw-r--r-- | cmd/lulu/main.go | 105 | ||||
| -rw-r--r-- | cmd/lulu/mfg.go | 74 | ||||
| -rw-r--r-- | cmd/lulu/pkgid.go | 80 | ||||
| -rw-r--r-- | cmd/lulu/text.go | 37 | ||||
| -rw-r--r-- | cmd/lulu/uint.go | 30 | ||||
| -rw-r--r-- | cmd/lulu/url.go | 27 |
9 files changed, 383 insertions, 247 deletions
diff --git a/cmd/lulu/bool.go b/cmd/lulu/bool.go new file mode 100644 index 0000000..20b1f2c --- /dev/null +++ b/cmd/lulu/bool.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + "fmt" + "strconv" +) + +type boolFlag interface { + flag.Value + IsBoolFlag() bool +} + +// boolValue implements flag.Value. +type boolValue struct { + p *bool + isSet bool +} + +func (v boolValue) String() string { + if v.p != nil { + return fmt.Sprint(*v.p) + } + return "" +} + +func (v *boolValue) Set(s string) error { + b, err := strconv.ParseBool(s) + if err != nil { + return err + } + *v.p = b + v.isSet = true + return nil +} + +func (v boolValue) IsSet() bool { return v.isSet } + +func (v boolValue) IsBoolFlag() bool { return true } diff --git a/cmd/lulu/duration.go b/cmd/lulu/duration.go new file mode 100644 index 0000000..a9aa375 --- /dev/null +++ b/cmd/lulu/duration.go @@ -0,0 +1,29 @@ +package main + +import ( + "time" +) + +type durationValue struct { + p *time.Duration + isSet bool +} + +func (v durationValue) String() string { + if v.p != nil { + return v.p.String() + } + return "" +} + +func (v *durationValue) Set(s string) error { + d, err := time.ParseDuration(s) + if err != nil { + return err + } + *v.p = d + v.isSet = true + return nil +} + +func (v durationValue) IsSet() bool { return v.isSet } diff --git a/cmd/lulu/flag.go b/cmd/lulu/flag.go index 0103dfc..a6592d7 100644 --- a/cmd/lulu/flag.go +++ b/cmd/lulu/flag.go @@ -1,24 +1,37 @@ package main import ( - "encoding" + "cmp" "flag" "fmt" - "net/url" + "io" + "maps" "os" - "strconv" + "slices" "strings" + "time" "unicode" + + "git.samanthony.xyz/lulu" ) -func nPagesFlag(nPages *uint) Flag { - return Flag{&uintValue{p: nPages}, "n", "Number of interior pages"} +func pkgIdFlag(p *lulu.PkgId, required bool) Flag { + return Flag{&pkgIdValue{p, false}, "mfg", fmt.Sprintf("Manufacturing options (%T)", *p), required} +} + +func nPagesFlag(p *uint, required bool) Flag { + return Flag{&uintValue{p: p}, "n", "Number of interior pages", required} +} + +func timeoutFlag(p *time.Duration, required bool) Flag { + return Flag{&durationValue{p, false}, "t", "Timeout", required} } type Flag struct { - val FlagValue - name string - usage string + val FlagValue + name string + usage string + required bool } type FlagValue interface { @@ -34,7 +47,7 @@ func (f Flag) Synopsis() string { return s } -func (f Flag) AddTo(fs *flag.FlagSet) { +func (f Flag) AddTo(fs *FlagSet) { fs.Var(f.val, f.name, f.usage) } @@ -42,135 +55,108 @@ func (f Flag) IsSet() bool { return f.val.IsSet() } -func (f Flag) MustBeSet(fs *flag.FlagSet) { +func (f Flag) MustBeSet(fs *FlagSet) { if !f.IsSet() { errMissingFlag(fs, f) } } -type boolFlag interface { - flag.Value - IsBoolFlag() bool -} - -// boolValue implements flag.Value. -type boolValue struct { - p *bool - isSet bool -} - -func (v boolValue) String() string { - if v.p != nil { - return fmt.Sprint(*v.p) +func (f Flag) MustNotBeSetWith(fs *FlagSet, other Flag) { + if f.IsSet() && other.IsSet() { + lg.Printf("lulu: %s -%s is incompatible with -%s\n", fs.Name(), f.name, other.name) + fs.Usage() + os.Exit(1) } - return "" } -func (v *boolValue) Set(s string) error { - b, err := strconv.ParseBool(s) - if err != nil { - return err - } - *v.p = b - v.isSet = true - return nil +type FlagDataType struct { + name string + format string + subtypes []FlagDataType } -func (v boolValue) IsSet() bool { return v.isSet } - -func (v boolValue) IsBoolFlag() bool { return true } - -type uintValue struct { - p *uint - isSet bool +type FlagDataTyper interface { + DataType() FlagDataType } -func (v uintValue) String() string { - if v.p != nil { - return fmt.Sprint(*v.p) - } - return "" +type FlagSet struct { + *flag.FlagSet + flags []Flag } -func (v *uintValue) Set(s string) error { - u, err := strconv.ParseUint(s, 10, 32) - if err != nil { - return err +func newFlagSet(name string, flags []Flag, synopses ...string) *FlagSet { + fs := &FlagSet{ + flag.NewFlagSet(name, flag.ExitOnError), + flags, } - *v.p = uint(u) - v.isSet = true - return nil -} - -func (v uintValue) IsSet() bool { return v.isSet } - -// textValue implements flag.Value for string types that implement -// TextUnmarshaler. -type textValue[T ~string, PT interface { - *T - encoding.TextUnmarshaler -}] struct { - p PT - isSet bool -} - -func newTextValue[T ~string, PT interface { - *T - encoding.TextUnmarshaler -}](p PT) *textValue[T, PT] { - return &textValue[T, PT]{p: p} -} - -func (v textValue[T, PT]) String() string { - if v.p != nil { - return string(*(*T)(v.p)) + for _, f := range flags { + fs.Var(f.val, f.name, f.usage) } - return "" -} - -func (v *textValue[T, PT]) Set(s string) error { - if err := v.p.UnmarshalText([]byte(s)); err != nil { - return err + fs.Usage = func() { + fmt.Fprintf(fs.Output(), "Usage of %s:\n", name) + for _, s := range synopses { + fmt.Fprintf(fs.Output(), " %s\n", strings.TrimSpace(s)) + } + if len(synopses) > 0 { + fmt.Fprintf(fs.Output(), "Flags:\n") + } + fs.PrintDefaults() + if dts := fs.DataTypes(); len(dts) > 0 { + fmt.Fprintf(fs.Output(), "Data Types:\n") + printDataTypes(fs.Output(), dts) + } } - v.isSet = true - return nil + return fs } -func (v textValue[T, PT]) IsSet() bool { return v.isSet } - -type urlValue struct { - p *url.URL - isSet bool +func printDataTypes(w io.Writer, dts []FlagDataType) { + for _, dt := range dts { + fmt.Fprintf(w, " %s\t%s\n", dt.name, dt.format) + } } -func (v urlValue) String() string { - if v.p != nil { - return v.p.String() +func (fs *FlagSet) DataTypes() []FlagDataType { + types := make(map[string]FlagDataType) + var insert func(dt FlagDataType) + insert = func(dt FlagDataType) { + types[dt.name] = dt + for _, subt := range dt.subtypes { + insert(subt) + } + } + for _, f := range fs.flags { + if dt, ok := f.val.(FlagDataTyper); ok { + insert(dt.DataType()) + } } - return "" + return sortMap(types) } -func (v *urlValue) Set(s string) error { - u, err := url.Parse(s) - if err != nil { - return err +func sortMap[K cmp.Ordered, V any](m map[K]V) []V { + s := make([]V, 0, len(m)) + for _, k := range slices.Sorted(maps.Keys(m)) { + s = append(s, m[k]) } - *v.p = *u - v.isSet = true - return nil + return s } -func (v urlValue) IsSet() bool { return v.isSet } +func (fs *FlagSet) Add(f Flag) { + fs.flags = append(fs.flags, f) + fs.Var(f.val, f.name, f.usage) +} -func mutexFlags(fs *flag.FlagSet, a, b Flag) { - if a.IsSet() && b.IsSet() { - lg.Printf("lulu: %s: -%s is incompatible with -%s\n", fs.Name(), a.name, b.name) - fs.Usage() - os.Exit(1) +func (fs *FlagSet) Parse(args []string) { + if err := fs.FlagSet.Parse(args); err != nil { + lg.Fatalf("lulu: %v\n", err) + } + for _, f := range fs.flags { + if f.required { + f.MustBeSet(fs) + } } } -func errMissingFlag(fs *flag.FlagSet, f Flag) { +func errMissingFlag(fs *FlagSet, f Flag) { lg.Printf("lulu %s: missing -%s flag\n", fs.Name(), f.name) fs.Usage() os.Exit(1) @@ -184,3 +170,12 @@ func firstWord(s string) string { } return s } + +func typeName(v any) string { + s := fmt.Sprintf("%T", v) + parts := strings.Split(s, ".") + if len(parts) > 1 { + return parts[len(parts)-1] + } + return parts[0] +} diff --git a/cmd/lulu/main.go b/cmd/lulu/main.go index 06d5ad4..7cf6bb3 100644 --- a/cmd/lulu/main.go +++ b/cmd/lulu/main.go @@ -18,7 +18,7 @@ var lg = log.New(os.Stderr, "", 0) type command struct { longhand, shorthand string - f func(clnt *lulu.Client, args []string) + f func(name string, clnt *lulu.Client, args []string) usage string } @@ -94,7 +94,7 @@ func main() { for _, cmd := range commands { switch cmdStr { case cmd.longhand, cmd.shorthand: - cmd.f(clnt, subArgs) + cmd.f(cmd.longhand, clnt, subArgs) return } } @@ -113,36 +113,27 @@ func flagBoolFunc(name, usage string, f func()) { }) } -func validateInterior(clnt *lulu.Client, args []string) { +func validateInterior(name string, clnt *lulu.Client, args []string) { var ( basic bool url url.URL - mfg = newMfgFlagSet() + mfg lulu.PkgId timeout time.Duration ) - basicFlag := Flag{&boolValue{p: &basic}, "basic", "Basic validation without pod_package_id manufacturing options"} - urlFlag := Flag{&urlValue{p: &url}, "url", "URL of interior file for Lulu to download"} - - fs := flag.NewFlagSet("validate-interior", flag.ExitOnError) - basicFlag.AddTo(fs) - urlFlag.AddTo(fs) - mfg.AddTo(fs) - fs.DurationVar(&timeout, "t", defaultTimeout, "Timeout") - - fs.Usage = func() { - out := fs.Output() - name := "validate-interior" - fmt.Fprintf(out, "Usage of %s:\n", name) - fmt.Fprintf(out, " %s %s %s [flags]\n", name, urlFlag.Synopsis(), mfg.Synopsis()) - fmt.Fprintf(out, " %s %s %s [flags]\n", name, basicFlag.Synopsis(), urlFlag.Synopsis()) - fmt.Fprintf(out, "Flags:\n") - fs.PrintDefaults() - } - - if err := fs.Parse(args); err != nil { - lg.Fatalf("lulu: %v\n", err) - } + basicFlag := Flag{&boolValue{p: &basic}, "basic", "Basic validation without pod_package_id manufacturing options", false} + urlFlag := Flag{&urlValue{p: &url}, "url", "URL of interior file for Lulu to download", true} + mfgFlag := pkgIdFlag(&mfg, false) + fs := newFlagSet(name, + []Flag{ + basicFlag, + urlFlag, + mfgFlag, + timeoutFlag(&timeout, false), + }, + fmt.Sprintf("%s %s %s [flags]\n", name, urlFlag.Synopsis(), mfgFlag.Synopsis()), + fmt.Sprintf(" %s %s %s [flags]\n", name, basicFlag.Synopsis(), urlFlag.Synopsis())) + fs.Parse(args) var val lulu.InteriorValidation var err error @@ -150,14 +141,12 @@ func validateInterior(clnt *lulu.Client, args []string) { defer cancel() if basic { urlFlag.MustBeSet(fs) - for _, f := range mfg.flags { - mutexFlags(fs, f, basicFlag) - } + mfgFlag.MustNotBeSetWith(fs, basicFlag) val, err = clnt.ValidateInteriorBasic(ctx, url.String()) } else { urlFlag.MustBeSet(fs) - mfg.MustBeSet(fs) - val, err = clnt.ValidateInterior(ctx, url.String(), mfg.PkgId()) + mfgFlag.MustBeSet(fs) + val, err = clnt.ValidateInterior(ctx, url.String(), mfg) } if err != nil { lg.Fatal(err) @@ -165,60 +154,44 @@ func validateInterior(clnt *lulu.Client, args []string) { fmt.Println(val) // TODO: output format? } -func validateCover(clnt *lulu.Client, args []string) { +func validateCover(name string, clnt *lulu.Client, args []string) { var ( url url.URL - mfg = newMfgFlagSet() + mfg lulu.PkgId nPages uint timeout time.Duration ) - - urlFlag := Flag{&urlValue{p: &url}, "url", "URL of cover file for Lulu to download"} - nPagesFlag := nPagesFlag(&nPages) - - fs := flag.NewFlagSet("validate-cover", flag.ExitOnError) - urlFlag.AddTo(fs) - mfg.AddTo(fs) - nPagesFlag.AddTo(fs) - fs.DurationVar(&timeout, "t", defaultTimeout, "Timeout") - - if err := fs.Parse(args); err != nil { - lg.Fatalf("lulu: %v\n", err) - } - urlFlag.MustBeSet(fs) - mfg.MustBeSet(fs) - nPagesFlag.MustBeSet(fs) + fs := newFlagSet(name, []Flag{ + Flag{&urlValue{p: &url}, "url", "URL of cover file for Lulu to download", true}, + pkgIdFlag(&mfg, true), + nPagesFlag(&nPages, true), + timeoutFlag(&timeout, false), + }) + fs.Parse(args) ctx, cancel := context.WithTimeout(clnt.Context(), timeout) defer cancel() - val, err := clnt.ValidateCover(ctx, url.String(), mfg.PkgId(), nPages) + val, err := clnt.ValidateCover(ctx, url.String(), mfg, nPages) if err != nil { lg.Fatal(err) } fmt.Println(val) // TODO: output format? } -func coverDimensions(clnt *lulu.Client, args []string) { +func coverDimensions(name string, clnt *lulu.Client, args []string) { var ( - mfg = newMfgFlagSet() + mfg lulu.PkgId nPages uint unit = lulu.Points ) - nPagesFlag := nPagesFlag(&nPages) - unitFlag := Flag{newTextValue(&unit), "u", fmt.Sprintf("Unit of measurement %v", lulu.UnitValues())} - - fs := flag.NewFlagSet("cover-dimensions", flag.ExitOnError) - mfg.AddTo(fs) - nPagesFlag.AddTo(fs) - unitFlag.AddTo(fs) - - if err := fs.Parse(args); err != nil { - lg.Fatalf("lulu: %v\n", err) - } - mfg.MustBeSet(fs) - nPagesFlag.MustBeSet(fs) + fs := newFlagSet(name, []Flag{ + pkgIdFlag(&mfg, true), + nPagesFlag(&nPages, true), + Flag{newTextValue(&unit), "u", fmt.Sprintf("Unit of measurement %v", lulu.UnitValues()), false}, + }) + fs.Parse(args) - dims, err := clnt.CoverDimensions(mfg.PkgId(), nPages, unit) + dims, err := clnt.CoverDimensions(mfg, nPages, unit) if err != nil { lg.Fatal(err) } diff --git a/cmd/lulu/mfg.go b/cmd/lulu/mfg.go deleted file mode 100644 index c94c42a..0000000 --- a/cmd/lulu/mfg.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "strings" - - "git.samanthony.xyz/lulu" -) - -// mfgFlags is a set of flags that specify a PkgId. -type mfgFlagSet struct { - size lulu.TrimSize - color lulu.ColorType - quality lulu.Quality - binding lulu.Binding - paper lulu.Paper - finish lulu.Finish - linen lulu.Linen - foil lulu.Foil - - flags []Flag -} - -func newMfgFlagSet() *mfgFlagSet { - mfg := new(mfgFlagSet) - mfg.flags = []Flag{ - {newTextValue(&mfg.size), "s", fmt.Sprintf("Trim size %v", lulu.TrimSizeValues())}, - {newTextValue(&mfg.color), "c", fmt.Sprintf("Color mode %v", lulu.ColorTypeValues())}, - {newTextValue(&mfg.quality), "q", fmt.Sprintf("Quality %v", lulu.QualityValues())}, - {newTextValue(&mfg.binding), "b", fmt.Sprintf("Binding %v", lulu.BindingValues())}, - {newTextValue(&mfg.paper), "p", fmt.Sprintf("Paper %v", lulu.PaperValues())}, - {newTextValue(&mfg.finish), "f", fmt.Sprintf("Cover finish %v", lulu.FinishValues())}, - {newTextValue(&mfg.linen), "l", fmt.Sprintf("Linen wrap color %v", lulu.LinenValues())}, - {newTextValue(&mfg.foil), "o", fmt.Sprintf("Foil color %v", lulu.FoilValues())}, - } - return mfg -} - -func (m mfgFlagSet) PkgId() lulu.PkgId { - return lulu.PkgId{ - m.size, - m.color, - m.quality, - m.binding, - m.paper, - m.finish, - m.linen, - m.foil, - } -} - -func (m mfgFlagSet) Synopsis() string { - s := new(strings.Builder) - for i, f := range m.flags { - if i != 0 { - fmt.Fprint(s, " ") - } - fmt.Fprint(s, f.Synopsis()) - } - return s.String() -} - -func (m mfgFlagSet) AddTo(fs *flag.FlagSet) { - for _, f := range m.flags { - f.AddTo(fs) - } -} - -func (m mfgFlagSet) MustBeSet(fs *flag.FlagSet) { - for _, f := range m.flags { - f.MustBeSet(fs) - } -} diff --git a/cmd/lulu/pkgid.go b/cmd/lulu/pkgid.go new file mode 100644 index 0000000..8681bfd --- /dev/null +++ b/cmd/lulu/pkgid.go @@ -0,0 +1,80 @@ +package main + +import ( + "fmt" + + "git.samanthony.xyz/lulu" +) + +type pkgIdValue struct { + p *lulu.PkgId + isSet bool +} + +func (v pkgIdValue) String() string { + if v.p != nil { + return v.p.String() + } + return "" +} + +func (v *pkgIdValue) Set(s string) error { + if err := v.p.UnmarshalText([]byte(s)); err != nil { + return err + } + v.isSet = true + return nil +} + +func (v pkgIdValue) IsSet() bool { return v.isSet } + +func (v pkgIdValue) DataType() FlagDataType { + return FlagDataType{ + typeName(*v.p), + fmt.Sprintf("%s.%s.%s.%s.%s.%s%s%s", + typeName(v.p.TrimSize), + typeName(v.p.ColorType), + typeName(v.p.Quality), + typeName(v.p.Binding), + typeName(v.p.Paper), + typeName(v.p.Finish), + typeName(v.p.Linen), + typeName(v.p.Foil), + ), + []FlagDataType{ + { + typeName(v.p.TrimSize), + fmt.Sprint(lulu.TrimSizeValues()), + nil, + }, { + typeName(v.p.ColorType), + fmt.Sprint(lulu.ColorTypeValues()), + nil, + }, { + typeName(v.p.Quality), + fmt.Sprint(lulu.QualityValues()), + nil, + }, { + typeName(v.p.Binding), + fmt.Sprint(lulu.BindingValues()), + nil, + }, { + typeName(v.p.Paper), + fmt.Sprint(lulu.PaperValues()), + nil, + }, { + typeName(v.p.Finish), + fmt.Sprint(lulu.FinishValues()), + nil, + }, { + typeName(v.p.Linen), + fmt.Sprint(lulu.LinenValues()), + nil, + }, { + typeName(v.p.Foil), + fmt.Sprint(lulu.FoilValues()), + nil, + }, + }, + } +} diff --git a/cmd/lulu/text.go b/cmd/lulu/text.go new file mode 100644 index 0000000..ee8d7d7 --- /dev/null +++ b/cmd/lulu/text.go @@ -0,0 +1,37 @@ +package main + +import "encoding" + +// textValue implements flag.Value for string types that implement +// TextUnmarshaler. +type textValue[T ~string, PT interface { + *T + encoding.TextUnmarshaler +}] struct { + p PT + isSet bool +} + +func newTextValue[T ~string, PT interface { + *T + encoding.TextUnmarshaler +}](p PT) *textValue[T, PT] { + return &textValue[T, PT]{p: p} +} + +func (v textValue[T, PT]) String() string { + if v.p != nil { + return string(*(*T)(v.p)) + } + return "" +} + +func (v *textValue[T, PT]) Set(s string) error { + if err := v.p.UnmarshalText([]byte(s)); err != nil { + return err + } + v.isSet = true + return nil +} + +func (v textValue[T, PT]) IsSet() bool { return v.isSet } diff --git a/cmd/lulu/uint.go b/cmd/lulu/uint.go new file mode 100644 index 0000000..91b958b --- /dev/null +++ b/cmd/lulu/uint.go @@ -0,0 +1,30 @@ +package main + +import ( + "fmt" + "strconv" +) + +type uintValue struct { + p *uint + isSet bool +} + +func (v uintValue) String() string { + if v.p != nil { + return fmt.Sprint(*v.p) + } + return "" +} + +func (v *uintValue) Set(s string) error { + u, err := strconv.ParseUint(s, 10, 32) + if err != nil { + return err + } + *v.p = uint(u) + v.isSet = true + return nil +} + +func (v uintValue) IsSet() bool { return v.isSet } diff --git a/cmd/lulu/url.go b/cmd/lulu/url.go new file mode 100644 index 0000000..1626269 --- /dev/null +++ b/cmd/lulu/url.go @@ -0,0 +1,27 @@ +package main + +import "net/url" + +type urlValue struct { + p *url.URL + isSet bool +} + +func (v urlValue) String() string { + if v.p != nil { + return v.p.String() + } + return "" +} + +func (v *urlValue) Set(s string) error { + u, err := url.Parse(s) + if err != nil { + return err + } + *v.p = *u + v.isSet = true + return nil +} + +func (v urlValue) IsSet() bool { return v.isSet } |