aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSam Anthony <sam@samanthony.xyz>2026-05-15 16:19:14 -0400
committerSam Anthony <sam@samanthony.xyz>2026-05-15 16:19:14 -0400
commitf24ab6ec8be2e8f478b5ab741c9f99d52006da61 (patch)
treeeba8b269ef4d59e0561c753852c6d35a04282a17
parent9e8867c3c6fe7a59e036e810836b8ee8f86ce10f (diff)
downloadlulu-f24ab6ec8be2e8f478b5ab741c9f99d52006da61.zip
cli: use kong for flag parsing
-rw-r--r--cmd/lulu/bool.go39
-rw-r--r--cmd/lulu/cli.go79
-rw-r--r--cmd/lulu/cost.go81
-rw-r--r--cmd/lulu/creds.go12
-rw-r--r--cmd/lulu/duration.go29
-rw-r--r--cmd/lulu/flag.go181
-rw-r--r--cmd/lulu/main.go218
-rw-r--r--cmd/lulu/pkgid.go80
-rw-r--r--cmd/lulu/ship.go37
-rw-r--r--cmd/lulu/testchecks/cost1
-rw-r--r--cmd/lulu/tests/cost1
-rw-r--r--cmd/lulu/text.go37
-rw-r--r--cmd/lulu/uint.go30
-rw-r--r--cmd/lulu/url.go27
-rw-r--r--cmd/lulu/vi.go67
-rw-r--r--go.mod1
-rw-r--r--go.sum2
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
+}
diff --git a/go.mod b/go.mod
index 9448402..8cef185 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index d6274a7..ed08a57 100644
--- a/go.sum
+++ b/go.sum
@@ -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=