From cebce1a92c1856d7b04113a5425e1a6a811fe77a Mon Sep 17 00:00:00 2001 From: Stavros Date: Mon, 15 Sep 2025 14:46:27 +0300 Subject: [PATCH] refactor: rework decoders using simpler and more efficient pattern --- internal/config/config.go | 20 ++-- internal/utils/decoders/decoders.go | 81 +++++++++++++++ internal/utils/decoders/decoders_test.go | 44 +++++++++ internal/utils/decoders/env_decoder.go | 121 +---------------------- internal/utils/decoders/flags_decoder.go | 121 ++--------------------- 5 files changed, 144 insertions(+), 243 deletions(-) create mode 100644 internal/utils/decoders/decoders.go create mode 100644 internal/utils/decoders/decoders_test.go diff --git a/internal/config/config.go b/internal/config/config.go index 1ebf028..4fc66fc 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -51,16 +51,16 @@ type Claims struct { } type OAuthServiceConfig struct { - ClientID string - ClientSecret string - ClientSecretFile string - Scopes []string - RedirectURL string - AuthURL string - TokenURL string - UserinfoURL string - InsecureSkipVerify bool - Name string + ClientID string `key:"client-id"` + ClientSecret string `key:"client-secret"` + ClientSecretFile string `key:"client-secret-file"` + Scopes []string `key:"scopes"` + RedirectURL string `key:"redirect-url"` + AuthURL string `key:"auth-url"` + TokenURL string `key:"token-url"` + UserinfoURL string `key:"user-info-url"` + InsecureSkipVerify bool `key:"insecure-skip-verify"` + Name string `key:"name"` } // User/session related stuff diff --git a/internal/utils/decoders/decoders.go b/internal/utils/decoders/decoders.go new file mode 100644 index 0000000..72a11d5 --- /dev/null +++ b/internal/utils/decoders/decoders.go @@ -0,0 +1,81 @@ +package decoders + +import ( + "reflect" + "strings" + "tinyauth/internal/config" +) + +func NormalizeKeys(keys map[string]string, rootName string, sep string) map[string]string { + normalized := make(map[string]string) + knownKeys := getKnownKeys() + + for k, v := range keys { + var finalKey []string + var suffix string + var camelClientName string + var camelField string + + finalKey = append(finalKey, rootName) + finalKey = append(finalKey, "providers") + cebabKey := strings.ToLower(k) + + for _, known := range knownKeys { + if strings.HasSuffix(cebabKey, strings.ReplaceAll(known, "-", sep)) { + suffix = known + break + } + } + + if suffix == "" { + continue + } + + clientNameParts := strings.Split(strings.TrimPrefix(strings.TrimSuffix(cebabKey, sep+strings.ReplaceAll(suffix, "-", sep)), "providers"+sep), sep) + + for i, p := range clientNameParts { + if i == 0 { + camelClientName += p + continue + } + if p == "" { + continue + } + camelClientName += strings.ToUpper(string([]rune(p)[0])) + string([]rune(p)[1:]) + } + + finalKey = append(finalKey, camelClientName) + + filedParts := strings.Split(suffix, "-") + + for i, p := range filedParts { + if i == 0 { + camelField += p + continue + } + if p == "" { + continue + } + camelField += strings.ToUpper(string([]rune(p)[0])) + string([]rune(p)[1:]) + } + + finalKey = append(finalKey, camelField) + normalized[strings.Join(finalKey, ".")] = v + } + + return normalized +} + +func getKnownKeys() []string { + var known []string + + p := config.OAuthServiceConfig{} + v := reflect.ValueOf(p) + typeOfP := v.Type() + + for field := range typeOfP.NumField() { + known = append(known, typeOfP.Field(field).Tag.Get("key")) + } + + return known +} diff --git a/internal/utils/decoders/decoders_test.go b/internal/utils/decoders/decoders_test.go new file mode 100644 index 0000000..285760c --- /dev/null +++ b/internal/utils/decoders/decoders_test.go @@ -0,0 +1,44 @@ +package decoders_test + +import ( + "testing" + "tinyauth/internal/utils/decoders" + + "gotest.tools/v3/assert" +) + +func TestNormalizeKeys(t *testing.T) { + // Test with env + test := map[string]string{ + "PROVIDERS_CLIENT1_CLIENT_ID": "my-client-id", + "PROVIDERS_CLIENT1_CLIENT_SECRET": "my-client-secret", + "PROVIDERS_MY_AWESOME_CLIENT_CLIENT_ID": "my-awesome-client-id", + "PROVIDERS_MY_AWESOME_CLIENT_CLIENT_SECRET_FILE": "/path/to/secret", + } + expected := map[string]string{ + "tinyauth.providers.client1.clientId": "my-client-id", + "tinyauth.providers.client1.clientSecret": "my-client-secret", + "tinyauth.providers.myAwesomeClient.clientId": "my-awesome-client-id", + "tinyauth.providers.myAwesomeClient.clientSecretFile": "/path/to/secret", + } + + normalized := decoders.NormalizeKeys(test, "tinyauth", "_") + assert.DeepEqual(t, normalized, expected) + + // Test with flags (assume -- is already stripped) + test = map[string]string{ + "providers-client1-client-id": "my-client-id", + "providers-client1-client-secret": "my-client-secret", + "providers-my-awesome-client-client-id": "my-awesome-client-id", + "providers-my-awesome-client-client-secret-file": "/path/to/secret", + } + expected = map[string]string{ + "tinyauth.providers.client1.clientId": "my-client-id", + "tinyauth.providers.client1.clientSecret": "my-client-secret", + "tinyauth.providers.myAwesomeClient.clientId": "my-awesome-client-id", + "tinyauth.providers.myAwesomeClient.clientSecretFile": "/path/to/secret", + } + + normalized = decoders.NormalizeKeys(test, "tinyauth", "-") + assert.DeepEqual(t, normalized, expected) +} diff --git a/internal/utils/decoders/env_decoder.go b/internal/utils/decoders/env_decoder.go index 938685a..4164aa5 100644 --- a/internal/utils/decoders/env_decoder.go +++ b/internal/utils/decoders/env_decoder.go @@ -1,37 +1,16 @@ package decoders import ( - "fmt" - "slices" - "sort" - "strings" "tinyauth/internal/config" "github.com/traefik/paerser/parser" ) -// Based on https://github.com/traefik/paerser/blob/master/parser/labels_decode.go - func DecodeEnv(env map[string]string) (config.Providers, error) { - normalized := normalizeEnv(env, "tinyauth") - - node, err := decodeEnvsToNode(normalized, "tinyauth", "tinyauth_providers") - - if err != nil { - return config.Providers{}, err - } - + normalized := NormalizeKeys(env, "tinyauth", "_") var providers config.Providers - metaOpts := parser.MetadataOpts{TagName: "env", AllowSliceAsStruct: true} - - err = parser.AddMetadata(&providers, node, metaOpts) - - if err != nil { - return config.Providers{}, err - } - - err = parser.Fill(&providers, node, parser.FillerOpts{AllowSliceAsStruct: true}) + err := parser.Decode(normalized, &providers, "tinyauth", "tinyauth.providers") if err != nil { return config.Providers{}, err @@ -39,99 +18,3 @@ func DecodeEnv(env map[string]string) (config.Providers, error) { return providers, nil } - -func decodeEnvsToNode(env map[string]string, rootName string, filters ...string) (*parser.Node, error) { - sorted := sortEnvKeys(env, filters) - - var node *parser.Node - - for i, k := range sorted { - split := strings.SplitN(k, "_", 4) // Something like PROVIDERS_MY_AWESOME_CLIENT is not supported because it will confuse the parser - - if split[0] != rootName { - return nil, fmt.Errorf("invalid env root %s", split[0]) - } - - if slices.Contains(split, "") { - return nil, fmt.Errorf("invalid element: %s", k) - } - - if i == 0 { - node = &parser.Node{} - } - - decodeEnvToNode(node, split, env[k]) - } - - return node, nil -} - -func decodeEnvToNode(root *parser.Node, path []string, value string) { - if len(root.Name) == 0 { - root.Name = path[0] - } - - if len(path) <= 1 { - root.Value = value - return - } - - if n := containsEnvNode(root.Children, path[1]); n != nil { - decodeEnvToNode(n, path[1:], value) - return - } - - child := &parser.Node{Name: path[1]} - decodeEnvToNode(child, path[1:], value) - root.Children = append(root.Children, child) -} - -func containsEnvNode(node []*parser.Node, name string) *parser.Node { - for _, n := range node { - if strings.EqualFold(n.Name, name) { - return n - } - } - return nil -} - -func sortEnvKeys(env map[string]string, filters []string) []string { - var sorted []string - - for k := range env { - if len(filters) == 0 { - sorted = append(sorted, k) - continue - } - - for _, f := range filters { - if strings.HasPrefix(k, f) { - sorted = append(sorted, k) - break - } - } - } - - sort.Strings(sorted) - return sorted -} - -// normalizeEnv converts env vars from PROVIDERS_CLIENT1_CLIENT_ID to tinyauth_providers_client_clientId -func normalizeEnv(env map[string]string, rootName string) map[string]string { - n := make(map[string]string) - for k, v := range env { - fk := strings.ToLower(k) - fks := strings.SplitN(fk, "_", 3) - fkb := "" - for i, s := range strings.Split(fks[len(fks)-1], "_") { - if i == 0 { - fkb += s - continue - } - fkb += strings.ToUpper(string([]rune(s)[0])) + string([]rune(s)[1:]) - } - fk = rootName + "_" + strings.Join(fks[:len(fks)-1], "_") + "_" + fkb - n[fk] = v - } - return n -} diff --git a/internal/utils/decoders/flags_decoder.go b/internal/utils/decoders/flags_decoder.go index 3b5550b..d973d29 100644 --- a/internal/utils/decoders/flags_decoder.go +++ b/internal/utils/decoders/flags_decoder.go @@ -1,37 +1,18 @@ package decoders import ( - "fmt" - "slices" - "sort" "strings" "tinyauth/internal/config" "github.com/traefik/paerser/parser" ) -// Based on https://github.com/traefik/paerser/blob/master/parser/labels_decode.go - func DecodeFlags(flags map[string]string) (config.Providers, error) { - normalized := normalizeFlags(flags, "tinyauth") - - node, err := decodeFlagsToNode(normalized, "tinyauth", "tinyauth_providers") - - if err != nil { - return config.Providers{}, err - } - + filtered := filterFlags(flags) + normalized := NormalizeKeys(filtered, "tinyauth", "-") var providers config.Providers - metaOpts := parser.MetadataOpts{TagName: "flag", AllowSliceAsStruct: true} - - err = parser.AddMetadata(&providers, node, metaOpts) - - if err != nil { - return config.Providers{}, err - } - - err = parser.Fill(&providers, node, parser.FillerOpts{AllowSliceAsStruct: true}) + err := parser.Decode(normalized, &providers, "tinyauth", "tinyauth.providers") if err != nil { return config.Providers{}, err @@ -40,98 +21,10 @@ func DecodeFlags(flags map[string]string) (config.Providers, error) { return providers, nil } -func decodeFlagsToNode(flags map[string]string, rootName string, filters ...string) (*parser.Node, error) { - sorted := sortFlagKeys(flags, filters) - - var node *parser.Node - - for i, k := range sorted { - split := strings.SplitN(k, "_", 4) // Something like --providers-my-awesome-client is not supported because it will confuse the parser - - if split[0] != rootName { - return nil, fmt.Errorf("invalid flag root %s", split[0]) - } - - if slices.Contains(split, "") { - return nil, fmt.Errorf("invalid element: %s", k) - } - - if i == 0 { - node = &parser.Node{} - } - - decodeFlagToNode(node, split, flags[k]) - } - - return node, nil -} - -func decodeFlagToNode(root *parser.Node, path []string, value string) { - if len(root.Name) == 0 { - root.Name = path[0] - } - - if len(path) <= 1 { - root.Value = value - return - } - - if n := containsFlagNode(root.Children, path[1]); n != nil { - decodeFlagToNode(n, path[1:], value) - return - } - - child := &parser.Node{Name: path[1]} - decodeFlagToNode(child, path[1:], value) - root.Children = append(root.Children, child) -} - -func containsFlagNode(node []*parser.Node, name string) *parser.Node { - for _, n := range node { - if strings.EqualFold(n.Name, name) { - return n - } - } - return nil -} - -func sortFlagKeys(flags map[string]string, filters []string) []string { - var sorted []string - - for k := range flags { - if len(filters) == 0 { - sorted = append(sorted, k) - continue - } - - for _, f := range filters { - if strings.HasPrefix(k, f) { - sorted = append(sorted, k) - break - } - } - } - - sort.Strings(sorted) - return sorted -} - -// normalizeFlags converts flags from --providers-client-client-id to tinyauth_providers_client_clientId -func normalizeFlags(flags map[string]string, rootName string) map[string]string { - n := make(map[string]string) +func filterFlags(flags map[string]string) map[string]string { + filtered := make(map[string]string) for k, v := range flags { - fk := strings.TrimPrefix(k, "--") - fks := strings.SplitN(fk, "-", 3) - fkb := "" - for i, s := range strings.Split(fks[len(fks)-1], "-") { - if i == 0 { - fkb += s - continue - } - fkb += strings.ToUpper(string([]rune(s)[0])) + string([]rune(s)[1:]) - } - fk = rootName + "_" + strings.Join(fks[:len(fks)-1], "_") + "_" + fkb - n[fk] = v + filtered[strings.TrimPrefix(k, "--")] = v } - return n + return filtered }