From 7a13146cb828223e10b7003687b2946bca7bef86 Mon Sep 17 00:00:00 2001 From: Tim Hockin Date: Thu, 14 Sep 2023 08:52:54 -0700 Subject: [PATCH] WIP: flag type --- main.go | 196 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 138 insertions(+), 58 deletions(-) diff --git a/main.go b/main.go index a201bd038..a414eb760 100644 --- a/main.go +++ b/main.go @@ -106,11 +106,145 @@ const ( const defaultDirMode = os.FileMode(0775) // subject to umask +// FIXME: should this carry SSH keys? if so, sub-structs? type credential struct { URL string `json:"url"` Username string `json:"username"` - Password string `json:"password"` - PasswordFile string `json:"password-file"` + Password string `json:"password,omitempty"` + PasswordFile string `json:"password-file,omitempty"` +} + +func (c credential) String() string { + jb, err := json.Marshal(c) + if err != nil { + return fmt.Sprintf("", err) + } + return string(jb) +} + +// credentialSliceValue is for flags. +type credentialSliceValue struct { + value []credential + changed bool +} + +var _ pflag.Value = &credentialSliceValue{} +var _ pflag.SliceValue = &credentialSliceValue{} + +// pflagCredentialSlice is like pflag.StringSlice() +func pflagCredentialSlice(name, def, usage string) *[]credential { + p := &credentialSliceValue{} + p.Set(def) + pflag.Var(p, name, usage) + return &p.value +} + +// unmarshal is like json.Unmarshal, but fails on unknown fields. +func (cs credentialSliceValue) unmarshal(val string, out any) error { + dec := json.NewDecoder(strings.NewReader(val)) + dec.DisallowUnknownFields() + return dec.Decode(out) +} + +// decodeList handles a string-encoded JSON object. +func (cs credentialSliceValue) decodeObject(val string) (credential, error) { + var cred credential + if err := cs.unmarshal(val, &cred); err != nil { + return credential{}, err + } + return cred, nil +} + +// decodeList handles a string-encoded JSON list. +func (cs credentialSliceValue) decodeList(val string) ([]credential, error) { + var creds []credential + if err := cs.unmarshal(val, &creds); err != nil { + return nil, err + } + return creds, nil +} + +// decode handles a string-encoded JSON object or list. +func (cs credentialSliceValue) decode(val string) ([]credential, error) { + s := strings.TrimSpace(val) + if s == "" { + return nil, nil + } + // If it tastes like an object... + if s[0] == '{' { + cred, err := cs.decodeObject(s) + return []credential{cred}, err + } + // If it tastes like a list... + if s[0] == '[' { + return cs.decodeList(s) + } + // Otherwise, bad + return nil, fmt.Errorf("not a JSON object or list") +} + +func (cs *credentialSliceValue) Set(val string) error { + v, err := cs.decode(val) + if err != nil { + return err + } + + if !cs.changed { + cs.value = v + } else { + cs.value = append(cs.value, v...) + } + cs.changed = true + + return nil +} + +func (cs credentialSliceValue) Type() string { + return "credentialSlice" +} + +func (cs credentialSliceValue) String() string { + if len(cs.value) == 0 { + return "[]" + } + jb, err := json.Marshal(cs.value) + if err != nil { + return fmt.Sprintf("", err) + } + return string(jb) +} + +func (cs *credentialSliceValue) Append(val string) error { + v, err := cs.decodeObject(val) + if err != nil { + return err + } + cs.value = append(cs.value, v) + return nil +} + +func (cs *credentialSliceValue) Replace(val []string) error { + creds := []credential{} + for _, s := range val { + v, err := cs.decodeObject(s) + if err != nil { + return err + } + creds = append(creds, v) + } + cs.value = creds + return nil +} + +func (cs credentialSliceValue) GetSlice() []string { + if len(cs.value) == 0 { + return nil + } + ret := []string{} + for _, cred := range cs.value { + ret = append(ret, cred.String()) + } + return ret } func envString(def string, key string, alts ...string) string { @@ -143,54 +277,6 @@ func envStringArray(def string, key string, alts ...string) []string { return parse(def) } -func envStringArrayJSONOrError(def string, key string, alts ...string) ([]string, error) { - parse := func(key, val string) ([]string, error) { - s := strings.TrimSpace(val) - if s == "" { - return nil, nil - } - // If it tastes like an object... - if s[0] == '{' { - return []string{s}, nil - } - // If it tastes like an array... - if s[0] == '[' { - // Parse into an array of "stuff to decode later". - var arr []json.RawMessage - if err := json.Unmarshal([]byte(s), &arr); err != nil { - return nil, fmt.Errorf("ERROR: invalid JSON list env %s=%q: %w", key, val, err) - } - // Re-encode as []string - ret := []string{} - for _, rm := range arr { - ret = append(ret, string(rm)) - } - return ret, nil - } - return nil, fmt.Errorf("ERROR: invalid JSON env %s=%q: not a list or object", key, val) - } - - if val := os.Getenv(key); val != "" { - return parse(key, val) - } - for _, alt := range alts { - if val := os.Getenv(alt); val != "" { - fmt.Fprintf(os.Stderr, "env %s has been deprecated, use %s instead\n", alt, key) - return parse(alt, val) - } - } - return parse("", def) -} -func envStringArrayJSON(def string, key string, alts ...string) []string { - val, err := envStringArrayJSONOrError(def, key, alts...) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - return nil - } - return val -} - func envBoolOrError(def bool, key string, alts ...string) (bool, error) { parse := func(key, val string) (bool, error) { parsed, err := strconv.ParseBool(val) @@ -506,9 +592,7 @@ func main() { flPasswordFile := pflag.String("password-file", envString("", "GITSYNC_PASSWORD_FILE", "GIT_SYNC_PASSWORD_FILE"), "the file from which the password or personal access token for git auth will be sourced") - flCredentials := pflag.StringArray("credential", - envStringArrayJSON("", "GITSYNC_CREDENTIAL"), - "one or more credentials (see --man for details) available for authentication") + flCredentials := pflagCredentialSlice("credential", envString("", "GITSYNC_CREDENTIAL"), "one or more credentials (see --man for details) available for authentication") flSSH := pflag.Bool("ssh", envBool(false, "GITSYNC_SSH", "GIT_SYNC_SSH"), @@ -760,11 +844,7 @@ func main() { credentials := []credential{} if len(*flCredentials) > 0 { - for _, s := range *flCredentials { - cred := credential{} - if err := json.Unmarshal([]byte(s), &cred); err != nil { - handleConfigError(log, true, "ERROR: can't parse --credential payload: %v", err) - } + for _, cred := range *flCredentials { if cred.URL == "" { //FIXME: can it default to --repo? handleConfigError(log, true, "ERROR: --credential URL must be specified")