diff options
| -rw-r--r-- | cmd/lulu/cancel.go | 11 | ||||
| -rw-r--r-- | cmd/lulu/cli.go | 56 | ||||
| -rw-r--r-- | cmd/lulu/cost.go | 15 | ||||
| -rw-r--r-- | cmd/lulu/job.go | 76 | ||||
| -rw-r--r-- | cmd/lulu/print.go | 71 | ||||
| -rwxr-xr-x | cmd/lulu/test | 14 | ||||
| -rw-r--r-- | cmd/lulu/testchecks/job | 6 | ||||
| -rw-r--r-- | cmd/lulu/testchecks/print | 1 | ||||
| -rw-r--r-- | cmd/lulu/testdata/cancel | 0 | ||||
| -rw-r--r-- | cmd/lulu/tests/cancel | 5 | ||||
| -rw-r--r-- | cmd/lulu/tests/job | 6 | ||||
| -rw-r--r-- | cmd/lulu/tests/print | 3 |
12 files changed, 191 insertions, 73 deletions
diff --git a/cmd/lulu/cancel.go b/cmd/lulu/cancel.go new file mode 100644 index 0000000..4e5d3b1 --- /dev/null +++ b/cmd/lulu/cancel.go @@ -0,0 +1,11 @@ +package main + +import "git.samanthony.xyz/lulu" + +type CancelCmd struct { + Id uint64 `arg help:"ID of the job to cancel"` +} + +func (cmd CancelCmd) Run(clnt *lulu.Client) error { + return clnt.Cancel(cmd.Id) +} diff --git a/cmd/lulu/cli.go b/cmd/lulu/cli.go index 0ff077a..9f384c6 100644 --- a/cmd/lulu/cli.go +++ b/cmd/lulu/cli.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "regexp" "strings" "github.com/alecthomas/kong" @@ -10,15 +11,42 @@ import ( ) var helpVars = kong.Vars{ - "cost_item_help": "Line items. Run 'lulu list cost-item' for format", - "default_timeout": defaultTimeout.String(), - "mfg_help": "Manufacturing settings. Run 'lulu list mfg' for format", - "npages_help": "Number of interior pages", - "order_status_help": fmt.Sprintf("Filter by order status %s", lulu.OrderStatusValues()), - "ship_level_help": fmt.Sprint(lulu.ShippingLevelValues()), - "unit_help": fmt.Sprintf("Unit of measurement: %s", lulu.UnitValues()), + "contact_help": "Email address that should be contacted if questions regarding the print job arise. Lulu recommends to use the email of a person who is responsible for placing the Print-Job like a developer or business owner.", + "cost_item_help": "Line items. Run 'lulu list cost-item' for format", + "default_timeout": defaultTimeout.String(), + "mfg_help": "Manufacturing settings. Run 'lulu list mfg' for format", + "npages_help": "Number of interior pages", + "order_status_help": fmt.Sprintf("Filter by order status %s", lulu.OrderStatusValues()), + "printable_help": "Line items. Run 'lulu list printable' for format", + "production_delay_help": fmt.Sprintf("Delay before job is sent to production after it is paid for (%s-%s)", lulu.MinProductionDelay, lulu.MaxProductionDelay), + "ship_level_help": fmt.Sprint(lulu.ShippingLevelValues()), + "unit_help": fmt.Sprintf("Unit of measurement: %s", lulu.UnitValues()), } +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.` + + printableExpr = regexp.MustCompile( + `^quantity\{([1-9][0-9]*)\}_extid\{([^},]+)\}_title\{([^},]+)\}_cover\{([^},]+)\}_interior\{([^},]+)\}_mfg\{(` + pkgIdExpr.String() + `)\}`) + + printableFmt = `PRINTABLE: quantity{<num>}_extid{<string>}_title{<string>}_cover{<url>}_interior{<url>}_mfg{<mfg>} +<n>: positive number +<string>: string of any characters except } and , +<url>: location of a file on the internet +<mfg>: run 'lulu list mfg' for valid values +E.g., "quantity{1}_extid{sku01}_title{The Title of the Book}_cover{https://example.com/cover.pdf}_interior{https://example.com/interior.pdf}_mfg{0600X0900.BW.STD.PB.060UW444.MXX}"` +) + type CLI struct { Globals @@ -26,8 +54,10 @@ type CLI struct { ValidateCover ValidateCoverCmd `cmd name:"vc" help:"Validate cover file"` CoverDimensions CoverDimensionsCmd `cmd name:"cd" help:"Calculate cover dimensions"` Cost CostCmd `cmd help:"Calculate the cost of a print job"` + Print PrintCmd `cmd help:"Submit a print job"` Jobs JobsCmd `cmd help:"Retrieve past print jobs"` Job JobCmd `cmd help:"Retrieve information about a particular print job"` + Cancel CancelCmd `cmd help:"Cancel a print job"` List ListCmd `cmd help:"Print a list of valid argument values"` } @@ -39,8 +69,9 @@ type Globals struct { } type ListCmd struct { - Mfg ListMfgCmd `cmd help:"List <mfg> format"` - CostItem ListCostItemCmd `cmd help:"List cost --items format"` + Mfg ListMfgCmd `cmd help:"List <mfg> format"` + CostItem ListCostItemCmd `cmd help:"List cost --items format"` + Printable ListPrintableCmd `cmd help:"List PRINTABLE format"` } type ListMfgCmd struct{} @@ -75,3 +106,10 @@ func typeName(v any) string { } return parts[0] } + +type ListPrintableCmd struct{} + +func (l ListPrintableCmd) Run() error { + fmt.Printf("%s\n", printableFmt) + return nil +} diff --git a/cmd/lulu/cost.go b/cmd/lulu/cost.go index 1826a5c..26f641e 100644 --- a/cmd/lulu/cost.go +++ b/cmd/lulu/cost.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "os" - "regexp" "strconv" "text/tabwriter" @@ -14,18 +13,6 @@ import ( "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}"` @@ -62,7 +49,7 @@ type PrintJobCostLineItem struct { func (item *PrintJobCostLineItem) UnmarshalText(text []byte) error { s := string(text) groups := printJobCostLineItemExpr.FindStringSubmatch(s) - if len(groups) != 4 { + if len(groups) != 3+1 { return fmt.Errorf("malformed %T: %q", *item, s) } diff --git a/cmd/lulu/job.go b/cmd/lulu/job.go index e58ec19..0678337 100644 --- a/cmd/lulu/job.go +++ b/cmd/lulu/job.go @@ -20,26 +20,31 @@ func (cmd JobCmd) Run(clnt *lulu.Client) error { } w := newIndentTabWriter(os.Stdout, 0) + print := func(w io.Writer, label string, val any) { + if s := fmt.Sprint(val); s != "" { + fmt.Fprintf(w, "%s:\t%s\t\n", label, s) + } + } fmtTime := func(t time.Time) string { return t.UTC().Format(time.RFC3339) } - fmt.Fprintf(w, "extid:\t%s\t\n", job.ExternalId) - fmt.Fprintf(w, "id:\t%d\t\n", job.Id) - fmt.Fprintf(w, "orderid:\t%s\t\n", job.OrderId) - fmt.Fprintf(w, "contact:\t%s\t\n", job.Contact) - fmt.Fprintf(w, "created:\t%s\t\n", fmtTime(job.Created)) - fmt.Fprintf(w, "modified:\t%s\t\n", fmtTime(job.Modified)) + print(w, "extid", job.ExternalId) + print(w, "id", job.Id) + print(w, "orderid", job.OrderId) + print(w, "contact", job.Contact) + print(w, "created", fmtTime(job.Created)) + print(w, "modified", fmtTime(job.Modified)) fmt.Fprintf(w, "items:\t\n") if err := w.Indent(); err != nil { return err } for _, item := range job.LineItems { - fmt.Fprintf(w, "extid:\t%s\t\n", item.ExternalId) - fmt.Fprintf(w, "id:\t%d\t\n", item.Id) - fmt.Fprintf(w, "printable-id:\t%s\t\n", item.PrintableId) - fmt.Fprintf(w, "mfg:\t%s\t\n", item.Mfg) - fmt.Fprintf(w, "quantity:\t%d\t\n", item.Quantity) - fmt.Fprintf(w, "npages:\t%d\t\n", item.NPages) - fmt.Fprintf(w, "title:\t%s\t\n", item.Title) + print(w, "extid", item.ExternalId) + print(w, "id", item.Id) + print(w, "printable-id", item.PrintableId) + print(w, "mfg", item.Mfg) + print(w, "quantity", item.Quantity) + print(w, "npages", item.NPages) + print(w, "title", item.Title) if !isEmptyNormalization(item.PrintableNormalization.Interior) { fmt.Fprintf(w, "interior-normalization:\t\n") if err := w.Indent(); err != nil { @@ -60,9 +65,7 @@ func (cmd JobCmd) Run(clnt *lulu.Client) error { return err } } - if item.TrackingId != "" { - fmt.Fprintf(w, "tracking-id:\t%s\t\n", item.TrackingId) - } + print(w, "tracking-id", item.TrackingId) if len(item.TrackingUrls) == 1 { fmt.Fprintf(w, "tracking-url:\t%s\t\n", item.TrackingUrls[0]) } else if len(item.TrackingUrls) > 1 { @@ -70,9 +73,8 @@ func (cmd JobCmd) Run(clnt *lulu.Client) error { fmt.Fprintf(w, "tracking-url-%d:\t%s\t\n", i, url) } } - if item.Carrier != "" { - fmt.Fprintf(w, "carrier:\t%s\t\n", item.Carrier) - } + print(w, "carrier", item.Carrier) + print(w, "status", item.Status) } if err := w.Unindent(); err != nil { return err @@ -87,39 +89,29 @@ func (cmd JobCmd) Run(clnt *lulu.Client) error { return err } - fmt.Fprintf(w, "production-delay:\t%s\t\n", job.ProductionDelay) - if !job.ProductionDue.IsZero() { - fmt.Fprintf(w, "production-due:\t%s\t\n", fmtTime(job.ProductionDue)) - } - fmt.Fprintf(w, "shipping-level:\t%s\t\n", job.ShipOpt) - if job.TaxCountry != "" { - fmt.Fprintf(w, "tax-country:\t%s\t\n", job.TaxCountry) - } - fmt.Fprintf(w, "status:\t%s\t\n", job.Status.Status) - fmt.Fprintf(w, "status-msg:\t%s\t\n", job.Status.Msg) - fmt.Fprintf(w, "status-changed:\t%s\t\n", fmtTime(job.Status.Changed)) + print(w, "production-delay", job.ProductionDelay) + print(w, "production-due", fmtTime(job.ProductionDue)) + print(w, "shipping-level", job.ShipOpt) + print(w, "tax-country", job.TaxCountry) + print(w, "status", job.Status.Status) + print(w, "status-msg", job.Status.Msg) + print(w, "status-changed", fmtTime(job.Status.Changed)) return w.Flush() } +func isEmptyNormalization(n lulu.NormalizationJob) bool { + return n.JobId == 0 && n.SrcMd5Sum == "" && n.SrcUrl == "" +} + func printNormalization(w io.Writer, n lulu.NormalizationJob) { if n.JobId != 0 { fmt.Fprintf(w, "jobid:\t%d\t\n", n.JobId) } - if n.NormalizedFile.Id != 0 { - fmt.Fprintf(w, "normalized-file-id:\t%d\t\n", n.NormalizedFile.Id) - } - if n.NormalizedFile.Name != "" { - fmt.Fprintf(w, "normalized-file-name:\t%s\t\n", n.NormalizedFile.Name) + if n.SrcUrl != "" { + fmt.Fprintf(w, "src-url:\t%s\t\n", n.SrcUrl) } if n.SrcMd5Sum != "" { fmt.Fprintf(w, "src-md5:\t%s\t\n", n.SrcMd5Sum) } - if n.SrcUrl != "" { - fmt.Fprintf(w, "src-url:\t%s\t\n", n.SrcUrl) - } -} - -func isEmptyNormalization(n lulu.NormalizationJob) bool { - return n.JobId == 0 && n.NormalizedFile.Id == 0 && n.NormalizedFile.Name == "" && n.SrcMd5Sum == "" && n.SrcUrl == "" } diff --git a/cmd/lulu/print.go b/cmd/lulu/print.go new file mode 100644 index 0000000..cbd536e --- /dev/null +++ b/cmd/lulu/print.go @@ -0,0 +1,71 @@ +package main + +import ( + "fmt" + "net/url" + "strconv" + "strings" + "time" + + "git.samanthony.xyz/lulu" +) + +type PrintCmd struct { + Contact lulu.EmailAddress `required help:"${contact_help}"` + ExtId string `required name:"extid" help:"External ID"` + ProductionDelay time.Duration `required help:"${production_delay_help}"` + ShippingAddress + Ship lulu.ShippingLevel `required help:"${ship_level_help}"` + Items []Printable `required placeholder:"PRINTABLE" help:"${printable_help}"` +} + +func (cmd PrintCmd) Run(clnt *lulu.Client) error { + items := make([]lulu.Printable, len(cmd.Items)) + for i := range cmd.Items { + items[i] = lulu.Printable(cmd.Items[i]) + } + job, err := clnt.Print(cmd.Contact, cmd.ExtId, cmd.ProductionDelay, cmd.ShippingAddress.Addr(), cmd.Ship, items) + if err != nil { + return err + } + fmt.Println(job.Id) + return nil +} + +type Printable lulu.Printable + +func (p *Printable) UnmarshalText(text []byte) error { + s := strings.TrimSpace(string(text)) + groups := printableExpr.FindStringSubmatch(s) + if len(groups) != 6+1 { + return fmt.Errorf("malformed %T: %q", *p, s) + } + + quantity, err := strconv.ParseUint(groups[1], 10, 32) + if err != nil { + return fmt.Errorf("invalid quantity in %T %q: %w", *p, s, err) + } + p.Quantity = uint(quantity) + + p.ExternalId = groups[2] + + p.Title = groups[3] + + cover, err := url.Parse(groups[4]) + if err != nil { + return fmt.Errorf("invalid cover URL in %T %q: %w", *p, s, err) + } + p.CoverUrl = cover.String() + + interior, err := url.Parse(groups[5]) + if err != nil { + return fmt.Errorf("invalid interior URL in %T %q: %w", *p, s, err) + } + p.InteriorUrl = interior.String() + + if err := p.Mfg.UnmarshalText([]byte(groups[6])); err != nil { + return fmt.Errorf("invalid %T in %T %q: %w", p.Mfg, *p, s, err) + } + + return nil +} diff --git a/cmd/lulu/test b/cmd/lulu/test index ee6b18d..3aacc99 100755 --- a/cmd/lulu/test +++ b/cmd/lulu/test @@ -10,6 +10,8 @@ export money='[0-9]+(\.[0-9]+)? [A-Z]{3}' export pkgid='[0-9]{4}X[0-9]{4}\.[A-Z]{2}\.[A-Z]{3}\.[A-Z]{2}\.[A-Z0-9]{5,8}\.[A-Z]{3}' export time='[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z' + + [[ ! -d testout ]] && mkdir testout [[ ! -d testerr ]] && mkdir testerr @@ -23,7 +25,11 @@ do out=testout/$base err=testerr/$base if [ -f $want ]; then # check against expected output - sh ${flags} $test >$out 2>$err + if ! sh ${flags} $test >$out 2>$err + then + echo "FAIL $test: error: $err" + exit 1 + fi if ! cmp -s $want $out then echo "FAIL $test: <Expected >Actual" @@ -31,7 +37,11 @@ do exit 1 fi elif [ -f $check ]; then # use script to check output - sh ${flags} $test >$out 2>$err + if ! sh ${flags} $test >$out 2>$err + then + echo "FAIL $test: error: $err" + exit 1 + fi echo "checking with $check..." >>$err if ! sh ${flags} $check $out >>$err then diff --git a/cmd/lulu/testchecks/job b/cmd/lulu/testchecks/job index 80d6cf9..1877251 100644 --- a/cmd/lulu/testchecks/job +++ b/cmd/lulu/testchecks/job @@ -1,6 +1,5 @@ grep -E '^extid: +[0-9a-zA-Z_-]+' $1 grep -E '^id: +[0-9]+' $1 -grep -E '^orderid: +[0-9]+' $1 grep -E "$(printf '^contact: +%s' "$email")" $1 grep -E "$(printf '^created: +%s' "$time")" $1 grep -E "$(printf '^modified: +%s' "$time")" $1 @@ -8,16 +7,15 @@ grep -E "$(printf '^modified: +%s' "$time")" $1 grep -E '^items: *$' $1 grep -E '^ extid: +[0-9a-zA-Z_-]+' $1 grep -E '^ id: +[0-9]+' $1 -grep -E '^ printable-id: +[0-9a-z-]+' $1 grep -E "$(printf '^ mfg: +%s' "$pkgid")" $1 grep -E '^ quantity: +[0-9]+' $1 grep -E '^ npages: +[0-9]+' $1 grep -E '^ title: +.+' $1 grep -E '^cost: *$' $1 -for field in "item-0" "shipping" "fulfillment" "discount" "tax" "total" +for field in "shipping" "fulfillment" "discount" "tax" "total" do - grep -E "$(printf '^ %s: +%s *$' "${field}" "${money}")" $1 + grep -E "$(printf '^ %s: +[0-9]+' "${field}")" $1 done grep -E "$(printf '^production-delay: +%s' "$duration")" $1 diff --git a/cmd/lulu/testchecks/print b/cmd/lulu/testchecks/print new file mode 100644 index 0000000..a1f5b73 --- /dev/null +++ b/cmd/lulu/testchecks/print @@ -0,0 +1 @@ +grep -E '^[1-9][0-9]*$' $1 diff --git a/cmd/lulu/testdata/cancel b/cmd/lulu/testdata/cancel new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/cmd/lulu/testdata/cancel diff --git a/cmd/lulu/tests/cancel b/cmd/lulu/tests/cancel new file mode 100644 index 0000000..bafb5ad --- /dev/null +++ b/cmd/lulu/tests/cancel @@ -0,0 +1,5 @@ +# Create a job and cancel it. +lulu -s print --contact admin@example.com --extid cancel-test --production-delay 2h \ + --country DE --city Lübeck --street1 'Holstenstr. 40' --postcode '23552' --name 'Hans Dampf' --phone 844-212-0689 --ship EXPRESS \ + --items 'quantity{1}_extid{cancel-test-sku01}_title{The Title of the Book}_cover{https://www.dropbox.com/sh/p3zh22vzsaegiri/AADP367j0bTWlt8fCu-_tm2ia/161025/139056_cover.pdf?dl=1}_interior{https://www.dropbox.com/sh/p3zh22vzsaegiri/AACOUn3LFKsITDzylh13bQpsa/161025/thesis2.pdf?dl=1}_mfg{0600X0900.BW.STD.PB.060UW444.MXX}' \ + | xargs lulu -s cancel diff --git a/cmd/lulu/tests/job b/cmd/lulu/tests/job index ac39591..236a2b1 100644 --- a/cmd/lulu/tests/job +++ b/cmd/lulu/tests/job @@ -1,3 +1,5 @@ -# Get info on the most recent job. -lulu -s jobs | tail -n1 | awk '{id=$3; print id}' \ +# Create a job and retrieve its information. +lulu -s print --contact admin@example.com --extid job-test --production-delay 2h \ + --country DE --city Lübeck --street1 'Holstenstr. 40' --postcode '23552' --name 'Hans Dampf' --phone 844-212-0689 --ship EXPRESS \ + --items 'quantity{1}_extid{job-test-sku01}_title{The Title of the Book}_cover{https://www.dropbox.com/sh/p3zh22vzsaegiri/AADP367j0bTWlt8fCu-_tm2ia/161025/139056_cover.pdf?dl=1}_interior{https://www.dropbox.com/sh/p3zh22vzsaegiri/AACOUn3LFKsITDzylh13bQpsa/161025/thesis2.pdf?dl=1}_mfg{0600X0900.BW.STD.PB.060UW444.MXX}' \ | xargs lulu -s job diff --git a/cmd/lulu/tests/print b/cmd/lulu/tests/print new file mode 100644 index 0000000..894534b --- /dev/null +++ b/cmd/lulu/tests/print @@ -0,0 +1,3 @@ +lulu -s print --contact admin@example.com --extid print-test --production-delay 2h \ + --country DE --city Lübeck --street1 'Holstenstr. 40' --postcode '23552' --name 'Hans Dampf' --phone 844-212-0689 --ship EXPRESS \ + --items 'quantity{1}_extid{print-test-sku01}_title{The Title of the Book}_cover{https://www.dropbox.com/s/7bv6mg2tj0h3l0r/lulu_trade_perfect_template.pdf?dl=1&raw=1}_interior{https://www.dropbox.com/s/r20orb8umqjzav9/lulu_trade_interior_template-32.pdf?dl=1&raw=1}_mfg{0600X0900.BW.STD.PB.060UW444.MXX},quantity{5}_extid{print-multi-test-sku02}_title{Another Title}_cover{https://www.dropbox.com/sh/p3zh22vzsaegiri/AADP367j0bTWlt8fCu-_tm2ia/161025/139056_cover.pdf?dl=1&raw=1}_interior{https://www.dropbox.com/sh/p3zh22vzsaegiri/AACOUn3LFKsITDzylh13bQpsa/161025/thesis2.pdf?dl=1&raw=1}_mfg{0600X0900.BW.STD.PB.060UW444.MXX}' |