diff options
| author | Sam Anthony <sam@samanthony.xyz> | 2026-05-15 16:19:14 -0400 |
|---|---|---|
| committer | Sam Anthony <sam@samanthony.xyz> | 2026-05-15 16:19:14 -0400 |
| commit | f24ab6ec8be2e8f478b5ab741c9f99d52006da61 (patch) | |
| tree | eba8b269ef4d59e0561c753852c6d35a04282a17 | |
| parent | 9e8867c3c6fe7a59e036e810836b8ee8f86ce10f (diff) | |
| download | lulu-f24ab6ec8be2e8f478b5ab741c9f99d52006da61.zip | |
cli: use kong for flag parsing
| -rw-r--r-- | cmd/lulu/bool.go | 39 | ||||
| -rw-r--r-- | cmd/lulu/cli.go | 79 | ||||
| -rw-r--r-- | cmd/lulu/cost.go | 81 | ||||
| -rw-r--r-- | cmd/lulu/creds.go | 12 | ||||
| -rw-r--r-- | cmd/lulu/duration.go | 29 | ||||
| -rw-r--r-- | cmd/lulu/flag.go | 181 | ||||
| -rw-r--r-- | cmd/lulu/main.go | 218 | ||||
| -rw-r--r-- | cmd/lulu/pkgid.go | 80 | ||||
| -rw-r--r-- | cmd/lulu/ship.go | 37 | ||||
| -rw-r--r-- | cmd/lulu/testchecks/cost | 1 | ||||
| -rw-r--r-- | cmd/lulu/tests/cost | 1 | ||||
| -rw-r--r-- | cmd/lulu/text.go | 37 | ||||
| -rw-r--r-- | cmd/lulu/uint.go | 30 | ||||
| -rw-r--r-- | cmd/lulu/url.go | 27 | ||||
| -rw-r--r-- | cmd/lulu/vi.go | 67 | ||||
| -rw-r--r-- | go.mod | 1 | ||||
| -rw-r--r-- | go.sum | 2 |
17 files changed, 292 insertions, 630 deletions
diff --git a/cmd/lulu/bool.go b/cmd/lulu/bool.go deleted file mode 100644 index 20b1f2c..0000000 --- a/cmd/lulu/bool.go +++ /dev/null @@ -1,39 +0,0 @@ -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/cli.go b/cmd/lulu/cli.go new file mode 100644 index 0000000..2cc0798 --- /dev/null +++ b/cmd/lulu/cli.go @@ -0,0 +1,79 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/alecthomas/kong" + + "git.samanthony.xyz/lulu" +) + +var helpVars = kong.Vars{ + "mfg_help": "Manufacturing settings. Run 'lulu list mfg' for valid values", + "ship_level_help": "Shipping speed. Run 'lulu list ship' for valid values", + "default_timeout": defaultTimeout.String(), +} + +type CLI struct { + Globals + + ValidateInterior ValidateInteriorCmd `cmd name:"vi" help:"Validate an interior file"` + //TODO ValidateCover ValidateCoverCmd `cmd name:"vc" help:"Validate a cover file"` + //TODO CoverDimensions CoverDimensionsCmd `cmd name:"cd" help:"Calculate the required dimensions of the cover"` + Cost CostCmd `cmd help:"Calculate the cost of a print job"` + + List ListCmd `cmd help:"Print a list of valid argument values"` +} + +type Globals struct { + Sandbox bool `short:"s" help:"Use the sandbox API environment"` + Debug bool `short:"d" help:"Print debug info to stdout"` + // TODO: -y flag +} + +type ListCmd struct { + Mfg ListMfgCmd `cmd help:"List <mfg> format"` + Ship ListShipCmd `cmd help:"List --ship values"` + CostItem ListCostItemCmd `cmd help:"List cost --items format"` +} + +type ListMfgCmd struct{} + +func (l ListMfgCmd) Run() error { + fmt.Printf( + `<mfg>: Size.Color.Quality.Binding.Paper.FinishLinenFoil +Size: %s +Color: %s +Quality: %s +Binding: %s +Paper: %s +Finish: %s +Linen: %s +Foil: %s +`, lulu.TrimSizeValues(), lulu.ColorTypeValues(), lulu.QualityValues(), lulu.BindingValues(), lulu.PaperValues(), lulu.FinishValues(), lulu.LinenValues(), lulu.FoilValues()) + return nil +} + +type ListShipCmd struct{} + +func (l ListShipCmd) Run() error { + fmt.Printf("--ship=%v\n", lulu.ShippingLevelValues()) + return nil +} + +type ListCostItemCmd struct{} + +func (l ListCostItemCmd) Run() error { + fmt.Printf("%s\n", printJobCostLineItemFmt) + return nil +} + +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/cost.go b/cmd/lulu/cost.go new file mode 100644 index 0000000..0f9a142 --- /dev/null +++ b/cmd/lulu/cost.go @@ -0,0 +1,81 @@ +package main + +import ( + "encoding/json" + "fmt" + "regexp" + "strconv" + + "github.com/alecthomas/kong" + + "git.samanthony.xyz/lulu" +) + +var ( + pkgIdExpr = regexp.MustCompile( + `[0-9]{4}X[0-9]{4}\.[A-Z]{2}\.[A-Z]{3}\.[A-Z]{2}\.[A-Z0-9]{5,8}\.[A-Z]{3}`) + printJobCostLineItemExpr = regexp.MustCompile( + `([1-9][0-9]*)x_(` + pkgIdExpr.String() + `)_p([1-9][0-9]*)`) + printJobCostLineItemFmt = `ITEM: <quantity> 'x_' <mfg> '_p' <npages> +<quantity>: positive number +<mfg>: run 'lulu list mfg' for valid values +<npages>: positive number +E.g., "10x_0600X0900.BW.STD.PB.060UW444.MXX_p250" is 10 copies with these manufacturing options and 250 pages.` +) + +type CostCmd struct { + ShippingAddress + Ship lulu.ShippingLevel `required help:"${ship_level_help}"` + Items []PrintJobCostLineItem `required help:"Line items. Run 'lulu list cost-item' for valid values"` +} + +func (cmd *CostCmd) Run(cli *kong.Kong, clnt *lulu.Client) error { + items := make([]lulu.PrintJobCostLineItem, len(cmd.Items)) + for i := range cmd.Items { + items[i] = cmd.Items[i].PrintJobCostLineItem + } + cost, addrVal, err := clnt.PrintJobCost(items, cmd.ShippingAddress.Addr(), cmd.Ship) + if err != nil { + return err + } + // TODO: -v flag to print everything + fmt.Printf("total: %s %s\n", cost.TotalCostInclTax, cost.Currency) + if len(addrVal.Warnings) > 0 { + j, err := json.Marshal(addrVal) + if err != nil { + cli.Fatalf("%v\n", err) + } + cli.Errorf("%s\n", j) + } + return nil +} + +type PrintJobCostLineItem struct { + lulu.PrintJobCostLineItem +} + +func (item *PrintJobCostLineItem) UnmarshalText(text []byte) error { + s := string(text) + groups := printJobCostLineItemExpr.FindStringSubmatch(s) + if len(groups) != 4 { + return fmt.Errorf("malformed %T: %q", *item, s) + } + + quantity, err := strconv.ParseUint(groups[1], 10, 32) + if err != nil { + return fmt.Errorf("invalid quantity in %T %q: %w", *item, s, err) + } + item.Quantity = uint(quantity) + + if err := item.Mfg.UnmarshalText([]byte(groups[2])); err != nil { + return fmt.Errorf("invalid %T in %T %q: %w", item.Mfg, *item, s, err) + } + + npages, err := strconv.ParseUint(groups[3], 10, 32) + if err != nil { + return fmt.Errorf("invalid number of pages in %T %q: %w", *item, s, err) + } + item.NPages = uint(npages) + + return nil +} diff --git a/cmd/lulu/creds.go b/cmd/lulu/creds.go index 775c4da..e26f0ce 100644 --- a/cmd/lulu/creds.go +++ b/cmd/lulu/creds.go @@ -23,16 +23,20 @@ type Credentials struct { key, secret string } -func readCreds() (Credentials, error) { - key, err := readFile(keyFile) +func readCreds(sandbox bool) (Credentials, error) { + keyf, secf := keyFile, secretFile + if sandbox { + keyf, secf = sandboxKeyFile, sandboxSecretFile + } + key, err := readFile(keyf) if err != nil { return Credentials{}, fmt.Errorf("error reading client-key file: %w", err) } - secret, err := readFile(secretFile) + sec, err := readFile(secf) if err != nil { return Credentials{}, fmt.Errorf("error reading client-secret file: %w", err) } - return Credentials{key, secret}, nil + return Credentials{key, sec}, nil } func readFile(path string) (string, error) { diff --git a/cmd/lulu/duration.go b/cmd/lulu/duration.go deleted file mode 100644 index a9aa375..0000000 --- a/cmd/lulu/duration.go +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index a16aa52..0000000 --- a/cmd/lulu/flag.go +++ /dev/null @@ -1,181 +0,0 @@ -package main - -import ( - "cmp" - "flag" - "fmt" - "io" - "maps" - "os" - "slices" - "strings" - "time" - "unicode" - - "git.samanthony.xyz/lulu" -) - -func pkgIdFlag(p *lulu.PkgId, required bool) Flag { - return Flag{&pkgIdValue{p, false}, "mfg", fmt.Sprintf("%s manufacturing options", typeName(*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 - required bool -} - -type FlagValue interface { - flag.Value - IsSet() bool -} - -func (f Flag) Synopsis() string { - s := fmt.Sprintf("-%s", f.name) - if bf, ok := f.val.(boolFlag); !ok || !bf.IsBoolFlag() { - s += " " + strings.ToUpper(firstWord(f.usage)) - } - return s -} - -func (f Flag) AddTo(fs *FlagSet) { - fs.Var(f.val, f.name, f.usage) -} - -func (f Flag) IsSet() bool { - return f.val.IsSet() -} - -func (f Flag) MustBeSet(fs *FlagSet) { - if !f.IsSet() { - errMissingFlag(fs, f) - } -} - -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) - } -} - -type FlagDataType struct { - name string - format string - subtypes []FlagDataType -} - -type FlagDataTyper interface { - DataType() FlagDataType -} - -type FlagSet struct { - *flag.FlagSet - flags []Flag -} - -func newFlagSet(name string, flags []Flag, synopses ...string) *FlagSet { - fs := &FlagSet{ - flag.NewFlagSet(name, flag.ExitOnError), - flags, - } - for _, f := range flags { - fs.Var(f.val, f.name, f.usage) - } - 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) - } - } - return fs -} - -func printDataTypes(w io.Writer, dts []FlagDataType) { - for _, dt := range dts { - fmt.Fprintf(w, " %s\t%s\n", dt.name, dt.format) - } -} - -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 sortMap(types) -} - -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]) - } - return s -} - -func (fs *FlagSet) Add(f Flag) { - fs.flags = append(fs.flags, f) - fs.Var(f.val, f.name, f.usage) -} - -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 *FlagSet, f Flag) { - lg.Printf("lulu %s: missing -%s flag\n", fs.Name(), f.name) - fs.Usage() - os.Exit(1) -} - -func firstWord(s string) string { - if i := strings.IndexFunc(s, func(r rune) bool { - return unicode.IsSpace(r) || unicode.IsPunct(r) - }); i >= 0 { - return s[:i] - } - 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 3ab4b8a..6a9bcba 100644 --- a/cmd/lulu/main.go +++ b/cmd/lulu/main.go @@ -2,219 +2,31 @@ package main import ( "context" - "flag" - "fmt" - "log" - "net/url" - "os" "time" + "github.com/alecthomas/kong" + "git.samanthony.xyz/lulu" ) const defaultTimeout = 15 * time.Second -var lg = log.New(os.Stderr, "", 0) - -type command struct { - longhand, shorthand string - f func(name string, clnt *lulu.Client, args []string) - usage string -} - -var commands = []command{ - { - "validate-interior", "vi", - validateInterior, - "Validate an interior file", - }, { - "validate-cover", "vc", - validateCover, - "Validate a cover file", - }, { - "cover-dimensions", "cd", - coverDimensions, - "Calculate the required dimensions of the cover", - }, -} - -var ( - sandbox = false - ask = true -) - -func usage() { - progName := os.Args[0] - out := flag.CommandLine.Output() - fmt.Fprintf(out, "Usage of %s:\n", progName) - fmt.Fprintf(out, " %s [global flags] [command] [command flags]\n", progName) - fmt.Fprintf(out, "Global Flags:\n") - flag.PrintDefaults() - fmt.Fprintf(out, "Commands:\n") - for _, cmd := range commands { - fmt.Fprintf(out, " %s / %s\n", cmd.longhand, cmd.shorthand) - fmt.Fprintf(out, " \t%s\n", cmd.usage) - } -} - func main() { - flag.Usage = usage - flag.BoolVar(&sandbox, "s", false, "Use the sandbox API environment") - flagBoolFunc("y", "Do not ask for confirmation", func() { ask = false }) - flag.BoolVar(&lulu.Debug, "d", false, "Print debug info to stdout") - flag.Parse() - - if sandbox { - lg.Println("lulu: using sandbox environment") - lulu.Sandbox() - keyFile = sandboxKeyFile - secretFile = sandboxSecretFile - } else { - lulu.Production() - } - - creds, err := readCreds() - if err != nil { - lg.Fatalf("%v\nCopy and paste your \"client key\" and \"client secret\" from the API Keys page into %s and %s\n%s\n", - err, keyFile, secretFile, lulu.ApiKeyPage()) - } - - clnt, err := lulu.NewClient(context.Background(), creds.key, creds.secret) - if err != nil { - lg.Fatal(err) - } - - args := os.Args[1+flag.NFlag():] // drop program name and flags - if len(args) < 1 { - flag.Usage() - os.Exit(1) - } - cmdStr := args[0] - subArgs := args[1:] - for _, cmd := range commands { - switch cmdStr { - case cmd.longhand, cmd.shorthand: - cmd.f(cmd.longhand, clnt, subArgs) - return - } - } - lg.Printf("lulu: unknown command %q\n", cmdStr) - flag.Usage() - os.Exit(1) -} - -func flagBoolFunc(name, usage string, f func()) { - flag.BoolFunc(name, usage, func(s string) error { - if s != "" { - return fmt.Errorf("no value allowed") - } - f() - return nil - }) -} - -func validateInterior(name string, clnt *lulu.Client, args []string) { - var ( - basic bool - url url.URL - mfg lulu.PkgId - timeout = defaultTimeout - ) - - 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 - ctx, cancel := context.WithTimeout(clnt.Context(), timeout) - defer cancel() - if basic { - urlFlag.MustBeSet(fs) - mfgFlag.MustNotBeSetWith(fs, basicFlag) - val, err = clnt.ValidateInteriorBasic(ctx, url.String()) - } else { - urlFlag.MustBeSet(fs) - mfgFlag.MustBeSet(fs) - val, err = clnt.ValidateInterior(ctx, url.String(), mfg) - } - if err != nil { - lg.Fatal(err) - } - - fmt.Println("status:", val.Status) - fmt.Println("id:", val.Id) - if len(val.ValidPkgIds) > 0 { - fmt.Printf("valid %ss: %v\n", typeName(val.ValidPkgIds[0]), val.ValidPkgIds) - } - for _, err := range val.Errors { - lg.Printf("lulu error: %s\n", err) - } - if len(val.Errors) > 0 || - (basic && val.Status != lulu.InteriorStatusValidated) || - val.Status != lulu.InteriorStatusNormalized { - os.Exit(1) - } -} - -func validateCover(name string, clnt *lulu.Client, args []string) { - var ( - url url.URL - mfg lulu.PkgId - nPages uint - timeout = defaultTimeout + var cli CLI + ctx := kong.Parse(&cli, + helpVars, + kong.ConfigureHelp(kong.HelpOptions{ + NoExpandSubcommands: true, + }), ) - 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, nPages) - if err != nil { - lg.Fatal(err) - } + lulu.Debug = cli.Debug - fmt.Println("status:", val.Status) - fmt.Println("id:", val.Id) - for _, err := range val.Errors { - lg.Printf("lulu error: %s\n", err) - } - if len(val.Errors) > 0 || val.Status != lulu.CoverStatusNormalized { - os.Exit(1) - } -} - -func coverDimensions(name string, clnt *lulu.Client, args []string) { - var ( - mfg lulu.PkgId - nPages uint - unit = lulu.Points - ) - 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) + creds, err := readCreds(cli.Sandbox) + ctx.FatalIfErrorf(err) + clnt, err := lulu.NewClient(context.Background(), creds.key, creds.secret) + ctx.FatalIfErrorf(err) - dims, err := clnt.CoverDimensions(mfg, nPages, unit) - if err != nil { - lg.Fatal(err) - } - fmt.Println(dims) + err = ctx.Run(cli.Globals, clnt) + ctx.FatalIfErrorf(err) } diff --git a/cmd/lulu/pkgid.go b/cmd/lulu/pkgid.go deleted file mode 100644 index 8681bfd..0000000 --- a/cmd/lulu/pkgid.go +++ /dev/null @@ -1,80 +0,0 @@ -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/ship.go b/cmd/lulu/ship.go new file mode 100644 index 0000000..f33aeaa --- /dev/null +++ b/cmd/lulu/ship.go @@ -0,0 +1,37 @@ +package main + +import "git.samanthony.xyz/lulu" + +type ShippingAddress struct { + Country string `required help:"2-letter country code"` + State string `help:"2- or 3-letter state/subdivision code"` + City string `required` + Street1 string `required name:"street1"` + Street2 string `name:"street2"` + PostCode string `required name:"postcode"` + IsBusiness bool `name:"business"` + Name string `required help:"First & last name"` + Title lulu.Title + Organization string + Email lulu.EmailAddress + Phone lulu.PhoneNumber `required` + TaxId string +} + +func (addr ShippingAddress) Addr() lulu.ShippingAddress { + return lulu.ShippingAddress{ + Country: addr.Country, + State: addr.State, + City: addr.City, + Street1: addr.Street1, + Street2: addr.Street2, + PostCode: addr.PostCode, + IsBusiness: addr.IsBusiness, + Name: addr.Name, + Title: addr.Title, + Organization: addr.Organization, + Email: addr.Email, + Phone: addr.Phone, + TaxId: addr.TaxId, + } +} diff --git a/cmd/lulu/testchecks/cost b/cmd/lulu/testchecks/cost new file mode 100644 index 0000000..4813c81 --- /dev/null +++ b/cmd/lulu/testchecks/cost @@ -0,0 +1 @@ +grep '^total: [1-9][0-9]*(\.[0-9]+)? [A-Z]{3}$' <$1 diff --git a/cmd/lulu/tests/cost b/cmd/lulu/tests/cost new file mode 100644 index 0000000..0be86a3 --- /dev/null +++ b/cmd/lulu/tests/cost @@ -0,0 +1 @@ +lulu -s cost --items 1x_0600X0900.BW.STD.PB.060UW444.MXX_p250,5x_0850X1100.BW.PRE.PB.060UC444.MXX_p75 --city Lübeck --country DE --name 'Hans Dampf' --phone 844-212-0689 --postcode '23552' --ship EXPRESS --street1 'Holstenstr. 40' diff --git a/cmd/lulu/text.go b/cmd/lulu/text.go deleted file mode 100644 index ee8d7d7..0000000 --- a/cmd/lulu/text.go +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index 91b958b..0000000 --- a/cmd/lulu/uint.go +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 1626269..0000000 --- a/cmd/lulu/url.go +++ /dev/null @@ -1,27 +0,0 @@ -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 } diff --git a/cmd/lulu/vi.go b/cmd/lulu/vi.go new file mode 100644 index 0000000..27e26ab --- /dev/null +++ b/cmd/lulu/vi.go @@ -0,0 +1,67 @@ +package main + +import ( + "context" + "fmt" + "net/url" + "time" + + "github.com/alecthomas/kong" + + "git.samanthony.xyz/lulu" +) + +type ValidateInteriorCmd struct { + Basic ValidateInteriorBasicCmd `cmd help:"Basic validation"` + Full ValidateInteriorFullCmd `cmd default:"withargs" help:"Extended validation (default)"` + Timeout time.Duration `short:"t" default:"${default_timeout}"` +} + +func (cmd ValidateInteriorCmd) AfterApply(ctx *kong.Context) error { + ctx.Bind(cmd.Timeout) + return nil +} + +type ValidateInteriorBasicCmd struct { + Url *url.URL `arg help:"Location of interior file"` +} + +func (cmd ValidateInteriorBasicCmd) Run(cli *kong.Kong, clnt *lulu.Client, timeout time.Duration) error { + return validateInterior(cli, clnt, timeout, func(ctx context.Context) (lulu.InteriorValidation, error) { + return clnt.ValidateInteriorBasic(ctx, cmd.Url.String()) + }, lulu.InteriorStatusValidated) +} + +type ValidateInteriorFullCmd struct { + Url *url.URL `arg help:"Location of interior file"` + Mfg lulu.PkgId `arg help:"${mfg_help}"` +} + +func (cmd ValidateInteriorFullCmd) Run(cli *kong.Kong, clnt *lulu.Client, timeout time.Duration) error { + return validateInterior(cli, clnt, timeout, func(ctx context.Context) (lulu.InteriorValidation, error) { + return clnt.ValidateInterior(ctx, cmd.Url.String(), cmd.Mfg) + }, lulu.InteriorStatusNormalized) +} + +func validateInterior(cli *kong.Kong, clnt *lulu.Client, timeout time.Duration, validate func(ctx context.Context) (lulu.InteriorValidation, error), wantStatus lulu.InteriorValidationStatus) error { + ctx, cancel := context.WithTimeout(clnt.Context(), timeout) + defer cancel() + val, err := validate(ctx) + if err != nil { + return err + } + fmt.Println("status:", val.Status) + fmt.Println("id:", val.Id) + if len(val.ValidPkgIds) > 0 { + fmt.Printf("valid %ss: %v\n", typeName(val.ValidPkgIds[0]), val.ValidPkgIds) + } + if len(val.Errors) > 0 { + for _, err := range val.Errors { + cli.Errorf("%v\n", err) + } + return fmt.Errorf("response contains errors") + } else if val.Status != wantStatus { + return fmt.Errorf("bad status %q; expected %q\n", val.Status, wantStatus) + } + return nil +} @@ -11,6 +11,7 @@ require ( require ( github.com/adrg/xdg v0.5.3 // indirect + github.com/alecthomas/kong v1.15.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.6 // indirect @@ -1,5 +1,7 @@ github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/alecthomas/kong v1.15.0 h1:BVJstKbpO73zKpmIu+m/aLRrNmWwxXPIGTNin9VmLVI= +github.com/alecthomas/kong v1.15.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |