aboutsummaryrefslogtreecommitdiffstats
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/lulu/creds.go41
-rw-r--r--cmd/lulu/flag.go138
-rw-r--r--cmd/lulu/main.go109
-rw-r--r--cmd/lulu/mfg.go74
-rw-r--r--cmd/lulu/validate_cover.go9
-rw-r--r--cmd/lulu/validate_interior.go59
6 files changed, 430 insertions, 0 deletions
diff --git a/cmd/lulu/creds.go b/cmd/lulu/creds.go
new file mode 100644
index 0000000..5d80018
--- /dev/null
+++ b/cmd/lulu/creds.go
@@ -0,0 +1,41 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/adrg/xdg"
+)
+
+var (
+ configDir = filepath.Join(xdg.ConfigHome, "lulu")
+ keyFile = filepath.Join(configDir, "key")
+ secretFile = filepath.Join(configDir, "secret")
+ sandboxKeyFile = filepath.Join(configDir, "sandbox", "key")
+ sandboxSecretFile = filepath.Join(configDir, "sandbox", "secret")
+)
+
+// Credentials contains the client-key and client-secret used to
+// authenticate the client (us) to the API.
+type Credentials struct {
+ key, secret string
+}
+
+func readCreds() (Credentials, error) {
+ key, err := readFile(keyFile)
+ if err != nil {
+ return Credentials{}, fmt.Errorf("error reading client-key file: %w", err)
+ }
+ secret, err := readFile(secretFile)
+ if err != nil {
+ return Credentials{}, fmt.Errorf("error reading client-secret file: %w", err)
+ }
+ return Credentials{key, secret}, nil
+}
+
+func readFile(path string) (string, error) {
+ data, err := os.ReadFile(path)
+ return strings.TrimSpace(string(data)), err
+}
diff --git a/cmd/lulu/flag.go b/cmd/lulu/flag.go
new file mode 100644
index 0000000..cd7773e
--- /dev/null
+++ b/cmd/lulu/flag.go
@@ -0,0 +1,138 @@
+package main
+
+import (
+ "encoding"
+ "flag"
+ "fmt"
+ "net/url"
+ "os"
+ "strconv"
+ "strings"
+ "unicode"
+)
+
+type Flag struct {
+ val flag.Value
+ name string
+ usage string
+}
+
+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 *flag.FlagSet) {
+ fs.Var(f.val, f.name, f.usage)
+}
+
+func (f Flag) IsSet() bool {
+ return f.val.String() != ""
+}
+
+func (f Flag) MustBeSet(fs *flag.FlagSet) {
+ if !f.IsSet() {
+ errMissingFlag(fs, f)
+ }
+}
+
+// boolValue implements flag.Value.
+type boolValue struct {
+ p *bool
+}
+
+func (bv boolValue) String() string {
+ if bv.p != nil {
+ return fmt.Sprint(*bv.p)
+ }
+ return fmt.Sprint(false)
+}
+
+func (bv boolValue) Set(s string) error {
+ b, err := strconv.ParseBool(s)
+ if err != nil {
+ return err
+ }
+ *bv.p = b
+ return nil
+}
+
+func (bv boolValue) IsBoolFlag() bool { return true }
+
+type boolFlag interface {
+ flag.Value
+ IsBoolFlag() bool
+}
+
+// textValue implements flag.Value for string types that implement
+// TextUnmarshaler.
+type textValue[T ~string, PT interface {
+ *T
+ encoding.TextUnmarshaler
+}] struct {
+ p PT
+}
+
+func newTextValue[T ~string, PT interface {
+ *T
+ encoding.TextUnmarshaler
+}](p PT) textValue[T, PT] {
+ return textValue[T, PT]{p}
+}
+
+func (tv textValue[T, PT]) String() string {
+ if tv.p != nil {
+ return string(*(*T)(tv.p))
+ }
+ return ""
+}
+
+func (tv textValue[T, PT]) Set(s string) error {
+ return tv.p.UnmarshalText([]byte(s))
+}
+
+type urlValue struct {
+ *url.URL
+}
+
+func (v urlValue) String() string {
+ if v.URL != nil {
+ return v.URL.String()
+ }
+ return ""
+}
+
+func (v urlValue) Set(s string) error {
+ u, err := url.Parse(s)
+ if err != nil {
+ return err
+ }
+ *v.URL = *u
+ return nil
+}
+
+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 errMissingFlag(fs *flag.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
+}
diff --git a/cmd/lulu/main.go b/cmd/lulu/main.go
new file mode 100644
index 0000000..b664644
--- /dev/null
+++ b/cmd/lulu/main.go
@@ -0,0 +1,109 @@
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "time"
+
+ "git.samanthony.xyz/lulu"
+)
+
+const defaultTimeout = 15 * time.Second
+
+var lg = log.New(os.Stderr, "", 0)
+
+type command struct {
+ longhand, shorthand string
+ f func(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",
+ },
+}
+
+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(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
+ })
+}
diff --git a/cmd/lulu/mfg.go b/cmd/lulu/mfg.go
new file mode 100644
index 0000000..c94c42a
--- /dev/null
+++ b/cmd/lulu/mfg.go
@@ -0,0 +1,74 @@
+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/validate_cover.go b/cmd/lulu/validate_cover.go
new file mode 100644
index 0000000..25afbfd
--- /dev/null
+++ b/cmd/lulu/validate_cover.go
@@ -0,0 +1,9 @@
+package main
+
+import (
+ "git.samanthony.xyz/lulu"
+)
+
+func validateCover(clnt *lulu.Client, args []string) {
+ lg.Fatal("not implemented") // TODO
+}
diff --git a/cmd/lulu/validate_interior.go b/cmd/lulu/validate_interior.go
new file mode 100644
index 0000000..f7c4bdb
--- /dev/null
+++ b/cmd/lulu/validate_interior.go
@@ -0,0 +1,59 @@
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "net/url"
+ "time"
+
+ "git.samanthony.xyz/lulu"
+)
+
+func validateInterior(clnt *lulu.Client, args []string) {
+ var (
+ basic bool
+ url url.URL
+ timeout time.Duration
+ )
+ basicFlag := Flag{boolValue{&basic}, "basic", "Basic validation without pod_package_id manufacturing options"}
+ urlFlag := Flag{urlValue{&url}, "url", "URL of interior file for Lulu to download"}
+ mfgFlags := newMfgFlagSet()
+ fs := flag.NewFlagSet("validate-interior", flag.ExitOnError)
+ basicFlag.AddTo(fs)
+ urlFlag.AddTo(fs)
+ mfgFlags.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(), mfgFlags.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", err)
+ }
+
+ var val lulu.InteriorValidation
+ var err error
+ ctx, cancel := context.WithTimeout(clnt.Context(), timeout)
+ defer cancel()
+ if basic {
+ urlFlag.MustBeSet(fs)
+ for _, f := range mfgFlags.flags {
+ mutexFlags(fs, f, basicFlag)
+ }
+ val, err = clnt.ValidateInteriorBasic(ctx, url.String())
+ } else {
+ urlFlag.MustBeSet(fs)
+ mfgFlags.MustBeSet(fs)
+ val, err = clnt.ValidateInterior(ctx, url.String(), mfgFlags.PkgId())
+ }
+ if err != nil {
+ lg.Fatal(err)
+ }
+ fmt.Println(val) // TODO: output format?
+}