pax_global_header00006660000000000000000000000064143625433340014521gustar00rootroot0000000000000052 comment=368ce2e07a1d7b673fe92bec9e5b752c8a73e710 paerser-0.2.0/000077500000000000000000000000001436254333400131615ustar00rootroot00000000000000paerser-0.2.0/.github/000077500000000000000000000000001436254333400145215ustar00rootroot00000000000000paerser-0.2.0/.github/workflows/000077500000000000000000000000001436254333400165565ustar00rootroot00000000000000paerser-0.2.0/.github/workflows/go-cross.yml000066400000000000000000000024211436254333400210340ustar00rootroot00000000000000name: Go Matrix on: push: branches: - master pull_request: jobs: cross: name: Go runs-on: ${{ matrix.os }} env: CGO_ENABLED: 0 strategy: matrix: go-version: [ 1.19, 1.x ] os: [ubuntu-latest, macos-latest, windows-latest] steps: # https://github.com/marketplace/actions/setup-go-environment - name: Set up Go ${{ matrix.go-version }} uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} # https://github.com/marketplace/actions/checkout - name: Checkout code uses: actions/checkout@v2 # https://github.com/marketplace/actions/cache - name: Cache Go modules uses: actions/cache@v3 with: # In order: # * Module download cache # * Build cache (Linux) # * Build cache (Mac) # * Build cache (Windows) path: | ~/go/pkg/mod ~/.cache/go-build ~/Library/Caches/go-build %LocalAppData%\go-build key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-${{ matrix.go-version }}-go- - name: Test run: go test -v -cover ./... paerser-0.2.0/.github/workflows/main.yml000066400000000000000000000025571436254333400202360ustar00rootroot00000000000000name: Main on: push: branches: - master pull_request: jobs: main: name: Main Process runs-on: ubuntu-latest env: GO_VERSION: 1.19 GOLANGCI_LINT_VERSION: v1.50.1 CGO_ENABLED: 0 steps: # https://github.com/marketplace/actions/setup-go-environment - name: Set up Go ${{ env.GO_VERSION }} uses: actions/setup-go@v2 with: go-version: ${{ env.GO_VERSION }} # https://github.com/marketplace/actions/checkout - name: Check out code uses: actions/checkout@v2 with: fetch-depth: 0 # https://github.com/marketplace/actions/cache - name: Cache Go modules uses: actions/cache@v3 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Check and get dependencies run: | go mod tidy git diff --exit-code go.mod git diff --exit-code go.sum go mod download # https://golangci-lint.run/usage/install#other-ci - name: Install golangci-lint ${{ env.GOLANGCI_LINT_VERSION }} run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_LINT_VERSION} - name: Make run: make paerser-0.2.0/.gitignore000066400000000000000000000000101436254333400151400ustar00rootroot00000000000000.idea/ paerser-0.2.0/.golangci.yml000066400000000000000000000030121436254333400155410ustar00rootroot00000000000000run: timeout: 2m skip-files: - ^unsecured/ linters-settings: govet: check-shadowing: true enable-all: true disable: - fieldalignment - reflectvaluecompare # can be enabled in the future gocyclo: min-complexity: 40 # 30 by default (but we recommend 10-20) goconst: min-len: 3 min-occurrences: 3 misspell: locale: US funlen: lines: -1 statements: 80 # default 40 gocognit: min-complexity: 65 # default 30 gofumpt: extra-rules: true godox: keywords: - FIXME linters: enable-all: true disable: - maligned # Deprecated - scopelint # Deprecated - golint # Deprecated - interfacer # Deprecated - exhaustivestruct # Deprecated - ifshort # Deprecated - varcheck # Deprecated - deadcode # Deprecated - structcheck # Deprecated - nosnakecase # Deprecated - cyclop # duplicate of gocyclo - rowserrcheck # SQL - sqlclosecheck # SQL - dupl - lll - nestif - gomnd - goerr113 - wrapcheck - nlreturn - wsl - exhaustive - exhaustruct - tparallel - testpackage - paralleltest - forcetypeassert - varnamelen issues: exclude-use-default: false max-per-linter: 0 max-same-issues: 0 exclude: - 'ST1000: at least one file in a package should have a package comment' - 'package-comments: should have a package comment' exclude-rules: - path: (.+)_test.go linters: - funlen - goconst - gosec - maintidx paerser-0.2.0/LICENSE000066400000000000000000000250051436254333400141700ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS Copyright 2022 Traefik Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. paerser-0.2.0/Makefile000066400000000000000000000002511436254333400146170ustar00rootroot00000000000000.PHONY: clean lint lint-fix test export GO111MODULE=on default: lint test test: go test -v -cover ./... lint: golangci-lint run lint-fix: golangci-lint run --fixpaerser-0.2.0/cli/000077500000000000000000000000001436254333400137305ustar00rootroot00000000000000paerser-0.2.0/cli/commands.go000066400000000000000000000071101436254333400160570ustar00rootroot00000000000000// Package cli provides tools to create commands that support advanced configuration features, // sub-commands, and allowing configuration from command-line flags, configuration files, and environment variables. package cli import ( "fmt" "io" "os" "path/filepath" ) // Command structure contains program/command information (command name and description). type Command struct { Name string Description string Configuration interface{} Resources []ResourceLoader Run func([]string) error CustomHelpFunc func(io.Writer, *Command) error Hidden bool // AllowArg if not set, disallows any argument that is not a known command or a sub-command. AllowArg bool subCommands []*Command } // AddCommand Adds a sub command. func (c *Command) AddCommand(cmd *Command) error { if c == nil || cmd == nil { return nil } if c.Name == cmd.Name { return fmt.Errorf("child command cannot have the same name as their parent: %s", cmd.Name) } c.subCommands = append(c.subCommands, cmd) return nil } // PrintHelp calls the custom help function of the command if it's set. // Otherwise, it calls the default help function. func (c *Command) PrintHelp(w io.Writer) error { if c.CustomHelpFunc != nil { return c.CustomHelpFunc(w, c) } return PrintHelp(w, c) } // Execute Executes a command. func Execute(cmd *Command) error { return execute(cmd, os.Args, true) } func execute(cmd *Command, args []string, root bool) error { // Calls command without args. if len(args) == 1 { if err := run(cmd, args[1:]); err != nil { return fmt.Errorf("command %s error: %w", args[0], err) } return nil } // Special case: if the command is the top level one, // and the first arg (`args[1]`) is not the command name or a known sub-command, // then we run the top level command itself. if root && cmd.Name != args[1] && !contains(cmd.subCommands, args[1]) { if err := run(cmd, args[1:]); err != nil { return fmt.Errorf("command %s error: %w", filepath.Base(args[0]), err) } return nil } // Calls command by its name. if len(args) >= 2 && cmd.Name == args[1] { if len(args) < 3 || !contains(cmd.subCommands, args[2]) { if err := run(cmd, args[2:]); err != nil { return fmt.Errorf("command %s error: %w", cmd.Name, err) } return nil } } // No sub-command, calls the current command. if len(cmd.subCommands) == 0 { if err := run(cmd, args[1:]); err != nil { return fmt.Errorf("command %s error: %w", cmd.Name, err) } return nil } // Trying to find the sub-command. for _, subCmd := range cmd.subCommands { if len(args) >= 2 && subCmd.Name == args[1] { return execute(subCmd, args, false) } if len(args) >= 3 && subCmd.Name == args[2] { return execute(subCmd, args[1:], false) } } return fmt.Errorf("command not found: %v", args) } func run(cmd *Command, args []string) error { if len(args) > 0 && !isFlag(args[0]) && !cmd.AllowArg { _ = cmd.PrintHelp(os.Stdout) return fmt.Errorf("command not found: %s", args[0]) } if isHelp(args) { return cmd.PrintHelp(os.Stdout) } if cmd.Run == nil { _ = cmd.PrintHelp(os.Stdout) return fmt.Errorf("command %s is not runnable", cmd.Name) } if cmd.Configuration == nil { return cmd.Run(args) } for _, resource := range cmd.Resources { done, err := resource.Load(args, cmd) if err != nil { return err } if done { break } } return cmd.Run(args) } func contains(cmds []*Command, name string) bool { for _, cmd := range cmds { if cmd.Name == name { return true } } return false } func isFlag(arg string) bool { return len(arg) > 0 && arg[0] == '-' } paerser-0.2.0/cli/commands_test.go000066400000000000000000000473251436254333400171320ustar00rootroot00000000000000package cli import ( "bytes" "errors" "fmt" "io" "os" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCommand_AddCommand(t *testing.T) { testCases := []struct { desc string subCommand *Command expectedError bool }{ { desc: "sub command nil", subCommand: nil, }, { desc: "add a simple command", subCommand: &Command{ Name: "sub", }, }, { desc: "add a sub command with the same name as their parent", subCommand: &Command{ Name: "root", }, expectedError: true, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() rootCmd := &Command{ Name: "root", } err := rootCmd.AddCommand(test.subCommand) if test.expectedError { require.Error(t, err) } else { require.NoError(t, err) } }) } } func TestCommand_PrintHelp(t *testing.T) { testCases := []struct { desc string command *Command expectedOutput string expectedError error }{ { desc: "print default help", command: &Command{}, expectedOutput: " \n\nUsage: [command] [flags] [arguments]\n\nUse \" [command] --help\" for help on any command.\n\n", }, { desc: "print custom help", command: &Command{ Name: "root", Description: "Description for root", Configuration: &struct { Foo []struct { Field string } }{}, Run: func(args []string) error { return nil }, CustomHelpFunc: func(w io.Writer, _ *Command) error { _, _ = fmt.Fprintln(w, "test") return nil }, }, expectedOutput: "test\n", }, { desc: "error is returned from called help", command: &Command{ CustomHelpFunc: func(_ io.Writer, _ *Command) error { return errors.New("test") }, }, expectedError: errors.New("test"), }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() buffer := &bytes.Buffer{} err := test.command.PrintHelp(buffer) assert.Equal(t, test.expectedError, err) assert.Equal(t, test.expectedOutput, buffer.String()) }) } } func Test_execute(t *testing.T) { var called string type expected struct { result string error bool } testCases := []struct { desc string args []string command func() *Command expected expected }{ { desc: "root command", args: []string{""}, command: func() *Command { return &Command{ Name: "root", Description: "This is a test", Configuration: nil, Run: func(_ []string) error { called = "root" return nil }, } }, expected: expected{result: "root"}, }, { desc: "root command, with argument, command not found", args: []string{"", "echo"}, command: func() *Command { return &Command{ Name: "root", Description: "This is a test", Configuration: nil, Run: func(_ []string) error { called = "root" return nil }, } }, expected: expected{error: true}, }, { desc: "root command, call help, with argument, command not found", args: []string{"", "echo", "--help"}, command: func() *Command { return &Command{ Name: "root", Description: "This is a test", Configuration: nil, Run: func(_ []string) error { called = "root" return nil }, } }, expected: expected{error: true}, }, { desc: "one sub command", args: []string{"", "sub1"}, command: func() *Command { rootCmd := &Command{ Name: "test", Description: "This is a test", Configuration: nil, Run: func(_ []string) error { called += "root" return nil }, } _ = rootCmd.AddCommand(&Command{ Name: "sub1", Description: "sub1", Configuration: nil, Run: func(_ []string) error { called += "sub1" return nil }, }) return rootCmd }, expected: expected{result: "sub1"}, }, { desc: "one sub command, with argument, command not found", args: []string{"", "sub1", "echo"}, command: func() *Command { rootCmd := &Command{ Name: "test", Description: "This is a test", Configuration: nil, Run: func(_ []string) error { called += "root" return nil }, } _ = rootCmd.AddCommand(&Command{ Name: "sub1", Description: "sub1", Configuration: nil, Run: func(_ []string) error { called += "sub1" return nil }, }) return rootCmd }, expected: expected{error: true}, }, { desc: "two sub commands", args: []string{"", "sub2"}, command: func() *Command { rootCmd := &Command{ Name: "test", Description: "This is a test", Configuration: nil, Run: func(_ []string) error { called += "root" return nil }, } _ = rootCmd.AddCommand(&Command{ Name: "sub1", Description: "sub1", Configuration: nil, Run: func(_ []string) error { called += "sub1" return nil }, }) _ = rootCmd.AddCommand(&Command{ Name: "sub2", Description: "sub2", Configuration: nil, Run: func(_ []string) error { called += "sub2" return nil }, }) return rootCmd }, expected: expected{result: "sub2"}, }, { desc: "command with sub sub-command, call sub command", args: []string{"", "sub1"}, command: func() *Command { rootCmd := &Command{ Name: "test", Description: "This is a test", Configuration: nil, Run: func(_ []string) error { called += "root" return nil }, } sub1 := &Command{ Name: "sub1", Description: "sub1", Configuration: nil, Run: func(_ []string) error { called += "sub1" return nil }, } _ = rootCmd.AddCommand(sub1) _ = sub1.AddCommand(&Command{ Name: "sub2", Description: "sub2", Configuration: nil, Run: func(_ []string) error { called += "sub2" return nil }, }) return rootCmd }, expected: expected{result: "sub1"}, }, { desc: "command with sub sub-command, call sub sub-command", args: []string{"", "sub1", "sub2"}, command: func() *Command { rootCmd := &Command{ Name: "test", Description: "This is a test", Configuration: nil, Run: func(_ []string) error { called += "root" return nil }, } sub1 := &Command{ Name: "sub1", Description: "sub1", Configuration: nil, Run: func(_ []string) error { called += "sub1" return nil }, } _ = rootCmd.AddCommand(sub1) _ = sub1.AddCommand(&Command{ Name: "sub2", Description: "sub2", Configuration: nil, Run: func(_ []string) error { called += "sub2" return nil }, }) return rootCmd }, expected: expected{result: "sub2"}, }, { desc: "command with sub command, call root command explicitly", args: []string{"", "root"}, command: func() *Command { rootCmd := &Command{ Name: "root", Description: "This is a test", Configuration: nil, Run: func(_ []string) error { called += "root" return nil }, } _ = rootCmd.AddCommand(&Command{ Name: "sub1", Description: "sub1", Configuration: nil, Run: func(_ []string) error { called += "sub1" return nil }, }) return rootCmd }, expected: expected{result: "root"}, }, { desc: "command with sub command, call root command implicitly", args: []string{""}, command: func() *Command { rootCmd := &Command{ Name: "root", Description: "This is a test", Configuration: nil, Run: func(_ []string) error { called += "root" return nil }, } _ = rootCmd.AddCommand(&Command{ Name: "sub1", Description: "sub1", Configuration: nil, Run: func(_ []string) error { called += "sub1" return nil }, }) return rootCmd }, expected: expected{result: "root"}, }, { desc: "command with sub command, call sub command which has no run", args: []string{"", "sub1"}, command: func() *Command { rootCmd := &Command{ Name: "root", Description: "This is a test", Configuration: nil, Run: func(_ []string) error { called += "root" return nil }, } _ = rootCmd.AddCommand(&Command{ Name: "sub1", Description: "sub1", Configuration: nil, }) return rootCmd }, expected: expected{error: true}, }, { desc: "command with sub command, call root command which has no run", args: []string{"", "root"}, command: func() *Command { rootCmd := &Command{ Name: "root", Description: "This is a test", Configuration: nil, } _ = rootCmd.AddCommand(&Command{ Name: "sub1", Description: "sub1", Configuration: nil, Run: func(_ []string) error { called += "sub1" return nil }, }) return rootCmd }, expected: expected{error: true}, }, { desc: "command with sub command, call implicitly root command which has no run", args: []string{""}, command: func() *Command { rootCmd := &Command{ Name: "root", Description: "This is a test", Configuration: nil, } _ = rootCmd.AddCommand(&Command{ Name: "sub1", Description: "sub1", Configuration: nil, Run: func(_ []string) error { called += "sub1" return nil }, }) return rootCmd }, expected: expected{error: true}, }, { desc: "command with sub command, call sub command with arguments", args: []string{"", "sub1", "foobar.txt"}, command: func() *Command { rootCmd := &Command{ Name: "root", Description: "This is a test", Configuration: nil, Run: func(_ []string) error { called = "root" return nil }, } _ = rootCmd.AddCommand(&Command{ Name: "sub1", Description: "sub1", Configuration: nil, AllowArg: true, Run: func(args []string) error { called += "sub1-" + strings.Join(args, "-") return nil }, }) return rootCmd }, expected: expected{result: "sub1-foobar.txt"}, }, { desc: "command with sub command, call root command with arguments", args: []string{"", "foobar.txt"}, command: func() *Command { rootCmd := &Command{ Name: "root", Description: "This is a test", Configuration: nil, AllowArg: true, Run: func(args []string) error { called += "root-" + strings.Join(args, "-") return nil }, } _ = rootCmd.AddCommand(&Command{ Name: "sub1", Description: "sub1", Configuration: nil, Run: func(args []string) error { called += "sub1-" + strings.Join(args, "-") return nil }, }) return rootCmd }, expected: expected{result: "root-foobar.txt"}, }, { desc: "command with sub command, call sub command with flags", args: []string{"", "sub1", "--foo=bar", "--fii=bir"}, command: func() *Command { rootCmd := &Command{ Name: "root", Description: "This is a test", Configuration: nil, Run: func(_ []string) error { called = "root" return nil }, } _ = rootCmd.AddCommand(&Command{ Name: "sub1", Description: "sub1", Configuration: nil, Run: func(args []string) error { called += "sub1-" + strings.Join(args, "") return nil }, }) return rootCmd }, expected: expected{result: "sub1---foo=bar--fii=bir"}, }, { desc: "command with sub command, call explicitly root command with flags", args: []string{"", "root", "--foo=bar", "--fii=bir"}, command: func() *Command { rootCmd := &Command{ Name: "root", Description: "This is a test", Configuration: nil, Run: func(args []string) error { called += "root-" + strings.Join(args, "") return nil }, } _ = rootCmd.AddCommand(&Command{ Name: "sub1", Description: "sub1", Configuration: nil, Run: func(args []string) error { called += "sub1-" + strings.Join(args, "") return nil }, }) return rootCmd }, expected: expected{result: "root---foo=bar--fii=bir"}, }, { desc: "command with sub command, call implicitly root command with flags", args: []string{"", "--foo=bar", "--fii=bir"}, command: func() *Command { rootCmd := &Command{ Name: "root", Description: "This is a test", Configuration: nil, Run: func(args []string) error { called += "root-" + strings.Join(args, "") return nil }, } _ = rootCmd.AddCommand(&Command{ Name: "sub1", Description: "sub1", Configuration: nil, Run: func(args []string) error { called += "sub1-" + strings.Join(args, "") return nil }, }) return rootCmd }, expected: expected{result: "root---foo=bar--fii=bir"}, }, { desc: "sub command help", args: []string{"", "test", "subtest", "--help"}, command: func() *Command { rootCmd := &Command{ Name: "test", Resources: []ResourceLoader{&FlagLoader{}}, } subCmd := &Command{ Name: "subtest", Resources: []ResourceLoader{&FlagLoader{}}, } err := rootCmd.AddCommand(subCmd) require.NoError(t, err) subSubCmd := &Command{ Name: "subsubtest", Resources: []ResourceLoader{&FlagLoader{}}, } err = subCmd.AddCommand(subSubCmd) require.NoError(t, err) subSubSubCmd := &Command{ Name: "subsubsubtest", Resources: []ResourceLoader{&FlagLoader{}}, Run: func([]string) error { called = "subsubsubtest" return nil }, } err = subSubCmd.AddCommand(subSubSubCmd) require.NoError(t, err) return rootCmd }, expected: expected{}, }, { desc: "sub sub-command help", args: []string{"", "test", "subtest", "subsubtest", "--help"}, command: func() *Command { rootCmd := &Command{ Name: "test", Resources: []ResourceLoader{&FlagLoader{}}, } subCmd := &Command{ Name: "subtest", Resources: []ResourceLoader{&FlagLoader{}}, } err := rootCmd.AddCommand(subCmd) require.NoError(t, err) subSubCmd := &Command{ Name: "subsubtest", Resources: []ResourceLoader{&FlagLoader{}}, } err = subCmd.AddCommand(subSubCmd) require.NoError(t, err) subSubSubCmd := &Command{ Name: "subsubsubtest", Resources: []ResourceLoader{&FlagLoader{}}, Run: func([]string) error { called = "subsubsubtest" return nil }, } err = subSubCmd.AddCommand(subSubSubCmd) require.NoError(t, err) return rootCmd }, expected: expected{}, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { defer func() { called = "" }() err := execute(test.command(), test.args, true) if test.expected.error { require.Error(t, err) } else { require.NoError(t, err) assert.Equal(t, test.expected.result, called) } }) } } func Test_execute_configuration(t *testing.T) { rootCmd := &Command{ Name: "root", Description: "This is a test", Configuration: nil, Run: func(_ []string) error { return nil }, } element := &Yo{ Fuu: "test", } sub1 := &Command{ Name: "sub1", Description: "sub1", Configuration: element, Resources: []ResourceLoader{&FlagLoader{}}, Run: func(args []string) error { return nil }, } err := rootCmd.AddCommand(sub1) require.NoError(t, err) args := []string{"", "sub1", "--foo=bar", "--fii=bir", "--yi"} err = execute(rootCmd, args, true) require.NoError(t, err) expected := &Yo{ Foo: "bar", Fii: "bir", Fuu: "test", Yi: &Yi{ Foo: "foo", Fii: "fii", }, } assert.Equal(t, expected, element) } func Test_execute_configuration_file(t *testing.T) { testCases := []struct { desc string args []string }{ { desc: "configFile arg in camel case", args: []string{"", "sub1", "--configFile=./fixtures/config.toml"}, }, { desc: "configfile arg in lower case", args: []string{"", "sub1", "--configfile=./fixtures/config.toml"}, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { rootCmd := &Command{ Name: "root", Description: "This is a test", Configuration: nil, Run: func(_ []string) error { return nil }, } element := &Yo{ Fuu: "test", } fileLoader := &FileLoader{ ConfigFileFlag: "configFile", BasePaths: []string{"/etc/traefik/traefik", "$XDG_CONFIG_HOME/traefik", "$HOME/.config/traefik", "./traefik"}, } sub1 := &Command{ Name: "sub1", Description: "sub1", Configuration: element, Resources: []ResourceLoader{fileLoader, &FlagLoader{}}, Run: func(args []string) error { return nil }, } err := rootCmd.AddCommand(sub1) require.NoError(t, err) err = execute(rootCmd, test.args, true) require.NoError(t, err) expected := &Yo{ Foo: "bar", Fii: "bir", Fuu: "test", Yi: &Yi{ Foo: "foo", Fii: "fii", }, } assert.Equal(t, expected, element) }) } } func Test_execute_help(t *testing.T) { element := &Yo{ Fuu: "test", } rooCmd := &Command{ Name: "root", Description: "Description for root", Configuration: element, Run: func(args []string) error { return nil }, } args := []string{"", "--help", "--foo"} backupStdout := os.Stdout defer func() { os.Stdout = backupStdout }() r, w, _ := os.Pipe() os.Stdout = w err := execute(rooCmd, args, true) if err != nil { return } // read and restore stdout if err = w.Close(); err != nil { t.Fatal(err) } out, err := io.ReadAll(r) if err != nil { t.Fatal(err) } os.Stdout = backupStdout assert.Equal(t, `root Description for root Usage: root [command] [flags] [arguments] Use "root [command] --help" for help on any command. Flag's usage: root [--flag=flag_argument] [-f [flag_argument]] # set flag_argument to flag(s) or: root [--flag[=true|false| ]] [-f [true|false| ]] # set true/false to boolean flag(s) Flags: --fii (Default: "fii") Fii description --foo (Default: "foo") Foo description --fuu (Default: "test") Fuu description --yi (Default: "false") --yi.fii (Default: "fii") --yi.foo (Default: "foo") --yi.fuu (Default: "") --yu.fii (Default: "fii") --yu.foo (Default: "foo") --yu.fuu (Default: "") `, string(out)) } func Test_execute_subCommands(t *testing.T) { rootCmd := &Command{ Name: "test", Resources: []ResourceLoader{&FlagLoader{}}, } subCmd := &Command{ Name: "subtest", Resources: []ResourceLoader{&FlagLoader{}}, } err := rootCmd.AddCommand(subCmd) require.NoError(t, err) subSubCmd := &Command{ Name: "subsubtest", Resources: []ResourceLoader{&FlagLoader{}}, Run: func([]string) error { return nil }, } err = subCmd.AddCommand(subSubCmd) require.NoError(t, err) subSubSubCmd := &Command{ Name: "subsubsubtest", Resources: []ResourceLoader{&FlagLoader{}}, Run: func([]string) error { return nil }, } err = subSubCmd.AddCommand(subSubSubCmd) require.NoError(t, err) err = execute(rootCmd, []string{"", "test", "subtest", "subsubtest", "subsubsubtest", "--help"}, true) require.NoError(t, err) } paerser-0.2.0/cli/file_finder.go000066400000000000000000000015721436254333400165320ustar00rootroot00000000000000package cli import ( "os" "path/filepath" "strings" ) // Finder holds a list of file paths. type Finder struct { BasePaths []string Extensions []string } // Find returns the first valid existing file among configFile // and the paths already registered with Finder. func (f Finder) Find(configFile string) (string, error) { paths := f.getPaths(configFile) for _, filePath := range paths { fp := os.ExpandEnv(filePath) _, err := os.Stat(fp) if os.IsNotExist(err) { continue } if err != nil { return "", err } return filepath.Abs(fp) } return "", nil } func (f Finder) getPaths(configFile string) []string { var paths []string if strings.TrimSpace(configFile) != "" { paths = append(paths, configFile) } for _, basePath := range f.BasePaths { for _, ext := range f.Extensions { paths = append(paths, basePath+"."+ext) } } return paths } paerser-0.2.0/cli/file_finder_test.go000066400000000000000000000074011436254333400175660ustar00rootroot00000000000000package cli import ( "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestFinder_Find(t *testing.T) { configFile, err := os.CreateTemp("", "traefik-file-finder-test-*.toml") require.NoError(t, err) defer func() { _ = os.Remove(configFile.Name()) }() dir, err := os.MkdirTemp("", "traefik-file-finder-test") require.NoError(t, err) defer func() { _ = os.RemoveAll(dir) }() fooFile, err := os.Create(filepath.Join(dir, "foo.toml")) require.NoError(t, err) _, err = os.Create(filepath.Join(dir, "bar.toml")) require.NoError(t, err) type expected struct { error bool path string } testCases := []struct { desc string basePaths []string configFile string expected expected }{ { desc: "not found: no config file", configFile: "", expected: expected{path: ""}, }, { desc: "not found: no config file, no other paths available", configFile: "", basePaths: []string{"/my/path/traefik", "$HOME/my/path/traefik", "./my-traefik"}, expected: expected{path: ""}, }, { desc: "not found: with non-existing config file", configFile: "/my/path/config.toml", expected: expected{path: ""}, }, { desc: "found: with config file", configFile: configFile.Name(), expected: expected{path: configFile.Name()}, }, { desc: "found: no config file, first base path", configFile: "", basePaths: []string{filepath.Join(dir, "foo"), filepath.Join(dir, "bar")}, expected: expected{path: fooFile.Name()}, }, { desc: "found: no config file, base path", configFile: "", basePaths: []string{"/my/path/traefik", "$HOME/my/path/traefik", filepath.Join(dir, "foo")}, expected: expected{path: fooFile.Name()}, }, { desc: "found: config file over base path", configFile: configFile.Name(), basePaths: []string{filepath.Join(dir, "foo"), filepath.Join(dir, "bar")}, expected: expected{path: configFile.Name()}, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { finder := Finder{ BasePaths: test.basePaths, Extensions: []string{"toml", "yaml", "yml"}, } path, err := finder.Find(test.configFile) if test.expected.error { require.Error(t, err) } else { require.NoError(t, err) assert.Equal(t, test.expected.path, path) } }) } } func TestFinder_getPaths(t *testing.T) { testCases := []struct { desc string basePaths []string configFile string expected []string }{ { desc: "no config file", basePaths: []string{"/etc/traefik/traefik", "$HOME/.config/traefik", "./traefik"}, configFile: "", expected: []string{ "/etc/traefik/traefik.toml", "/etc/traefik/traefik.yaml", "/etc/traefik/traefik.yml", "$HOME/.config/traefik.toml", "$HOME/.config/traefik.yaml", "$HOME/.config/traefik.yml", "./traefik.toml", "./traefik.yaml", "./traefik.yml", }, }, { desc: "with config file", basePaths: []string{"/etc/traefik/traefik", "$HOME/.config/traefik", "./traefik"}, configFile: "/my/path/config.toml", expected: []string{ "/my/path/config.toml", "/etc/traefik/traefik.toml", "/etc/traefik/traefik.yaml", "/etc/traefik/traefik.yml", "$HOME/.config/traefik.toml", "$HOME/.config/traefik.yaml", "$HOME/.config/traefik.yml", "./traefik.toml", "./traefik.yaml", "./traefik.yml", }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() finder := Finder{ BasePaths: test.basePaths, Extensions: []string{"toml", "yaml", "yml"}, } paths := finder.getPaths(test.configFile) assert.Equal(t, test.expected, paths) }) } } paerser-0.2.0/cli/fixtures/000077500000000000000000000000001436254333400156015ustar00rootroot00000000000000paerser-0.2.0/cli/fixtures/config.toml000066400000000000000000000000351436254333400177410ustar00rootroot00000000000000foo = "bar" fii = "bir" [yi] paerser-0.2.0/cli/fixtures_test.go000066400000000000000000000006241436254333400171710ustar00rootroot00000000000000package cli type Yo struct { Foo string `description:"Foo description"` Fii string `description:"Fii description"` Fuu string `description:"Fuu description"` Yi *Yi `label:"allowEmpty" file:"allowEmpty"` Yu *Yi } func (y *Yo) SetDefaults() { y.Foo = "foo" y.Fii = "fii" } type Yi struct { Foo string Fii string Fuu string } func (y *Yi) SetDefaults() { y.Foo = "foo" y.Fii = "fii" } paerser-0.2.0/cli/help.go000066400000000000000000000040151436254333400152070ustar00rootroot00000000000000package cli import ( "io" "strings" "text/tabwriter" "text/template" "github.com/Masterminds/sprig/v3" "github.com/traefik/paerser/flag" "github.com/traefik/paerser/generator" "github.com/traefik/paerser/parser" ) const tmplHelp = `{{ .Cmd.Name }} {{ .Cmd.Description }} Usage: {{ .Cmd.Name }} [command] [flags] [arguments] Use "{{ .Cmd.Name }} [command] --help" for help on any command. {{if .SubCommands }} Commands: {{- range $i, $subCmd := .SubCommands }} {{ if not $subCmd.Hidden }} {{ $subCmd.Name }} {{ $subCmd.Description }}{{end}}{{end}} {{end}} {{- if .Flags }} Flag's usage: {{ .Cmd.Name }} [--flag=flag_argument] [-f [flag_argument]] # set flag_argument to flag(s) or: {{ .Cmd.Name }} [--flag[=true|false| ]] [-f [true|false| ]] # set true/false to boolean flag(s) Flags: {{- range $i, $flag := .Flags }} --{{ SliceIndexN $flag.Name }} {{if ne $flag.Name "global.sendanonymoususage"}}(Default: "{{ $flag.Default}}"){{end}} {{if $flag.Description }} {{ wrapWith 80 "\n\t\t" $flag.Description }} {{else}} {{- end}} {{- end}} {{- end}} ` func isHelp(args []string) bool { for _, name := range args { if name == "--help" || name == "-help" || name == "-h" { return true } } return false } // PrintHelp prints the help for the command given as argument. func PrintHelp(w io.Writer, cmd *Command) error { var flags []parser.Flat if cmd.Configuration != nil { generator.Generate(cmd.Configuration) var err error flags, err = flag.Encode(cmd.Configuration) if err != nil { return err } } model := map[string]interface{}{ "Cmd": cmd, "Flags": flags, "SubCommands": cmd.subCommands, } funcs := sprig.TxtFuncMap() funcs["SliceIndexN"] = sliceIndexN tmpl, err := template.New("flags"). Funcs(funcs). Parse(tmplHelp) if err != nil { return err } tw := tabwriter.NewWriter(w, 4, 0, 4, ' ', 0) err = tmpl.Execute(tw, model) if err != nil { return err } return tw.Flush() } func sliceIndexN(flag string) string { return strings.ReplaceAll(flag, "[0]", "[n]") } paerser-0.2.0/cli/help_test.go000066400000000000000000000104201436254333400162430ustar00rootroot00000000000000package cli import ( "bytes" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestPrintHelp(t *testing.T) { testCases := []struct { desc string command *Command expected string }{ { desc: "no sub-command, with flags", command: func() *Command { element := &Yo{ Fuu: "test", } return &Command{ Name: "root", Description: "Description for root", Configuration: element, Run: func(args []string) error { return nil }, } }(), expected: `root Description for root Usage: root [command] [flags] [arguments] Use "root [command] --help" for help on any command. Flag's usage: root [--flag=flag_argument] [-f [flag_argument]] # set flag_argument to flag(s) or: root [--flag[=true|false| ]] [-f [true|false| ]] # set true/false to boolean flag(s) Flags: --fii (Default: "fii") Fii description --foo (Default: "foo") Foo description --fuu (Default: "test") Fuu description --yi (Default: "false") --yi.fii (Default: "fii") --yi.foo (Default: "foo") --yi.fuu (Default: "") --yu.fii (Default: "fii") --yu.foo (Default: "foo") --yu.fuu (Default: "") `, }, { desc: "with sub-commands, with flags, call root help", command: func() *Command { element := &Yo{ Fuu: "test", } rootCmd := &Command{ Name: "root", Description: "Description for root", Configuration: element, Run: func(_ []string) error { return nil }, } err := rootCmd.AddCommand(&Command{ Name: "sub1", Description: "Description for sub1", Configuration: element, Run: func(args []string) error { return nil }, }) require.NoError(t, err) err = rootCmd.AddCommand(&Command{ Name: "sub2", Description: "Description for sub2", Configuration: element, Run: func(args []string) error { return nil }, }) require.NoError(t, err) return rootCmd }(), expected: `root Description for root Usage: root [command] [flags] [arguments] Use "root [command] --help" for help on any command. Commands: sub1 Description for sub1 sub2 Description for sub2 Flag's usage: root [--flag=flag_argument] [-f [flag_argument]] # set flag_argument to flag(s) or: root [--flag[=true|false| ]] [-f [true|false| ]] # set true/false to boolean flag(s) Flags: --fii (Default: "fii") Fii description --foo (Default: "foo") Foo description --fuu (Default: "test") Fuu description --yi (Default: "false") --yi.fii (Default: "fii") --yi.foo (Default: "foo") --yi.fuu (Default: "") --yu.fii (Default: "fii") --yu.foo (Default: "foo") --yu.fuu (Default: "") `, }, { desc: "no sub-command, no flags", command: func() *Command { return &Command{ Name: "root", Description: "Description for root", Configuration: nil, Run: func(args []string) error { return nil }, } }(), expected: `root Description for root Usage: root [command] [flags] [arguments] Use "root [command] --help" for help on any command. `, }, { desc: "no sub-command, slice flags", command: func() *Command { return &Command{ Name: "root", Description: "Description for root", Configuration: &struct { Foo []struct { Field string } }{}, Run: func(args []string) error { return nil }, } }(), expected: `root Description for root Usage: root [command] [flags] [arguments] Use "root [command] --help" for help on any command. Flag's usage: root [--flag=flag_argument] [-f [flag_argument]] # set flag_argument to flag(s) or: root [--flag[=true|false| ]] [-f [true|false| ]] # set true/false to boolean flag(s) Flags: --foo (Default: "") --foo[n].field (Default: "") `, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() buffer := &bytes.Buffer{} err := PrintHelp(buffer, test.command) require.NoError(t, err) assert.Equal(t, test.expected, buffer.String()) }) } } paerser-0.2.0/cli/loader.go000066400000000000000000000003341436254333400155250ustar00rootroot00000000000000package cli // ResourceLoader is a configuration resource loader. type ResourceLoader interface { // Load populates cmd.Configuration, optionally using args to do so. Load(args []string, cmd *Command) (bool, error) } paerser-0.2.0/cli/loader_env.go000066400000000000000000000013661436254333400164030ustar00rootroot00000000000000package cli import ( "fmt" "os" "github.com/traefik/paerser/env" ) // EnvLoader loads a configuration from all the environment variables prefixed with Prefix (default: "TRAEFIK_"). type EnvLoader struct { Prefix string } // Load loads the command's configuration from the environment variables. func (e *EnvLoader) Load(_ []string, cmd *Command) (bool, error) { prefix := env.DefaultNamePrefix if e.Prefix != "" { prefix = e.Prefix } vars := env.FindPrefixedEnvVars(os.Environ(), prefix, cmd.Configuration) if len(vars) == 0 { return false, nil } if err := env.Decode(vars, prefix, cmd.Configuration); err != nil { return false, fmt.Errorf("failed to decode configuration from environment variables: %w ", err) } return true, nil } paerser-0.2.0/cli/loader_file.go000066400000000000000000000040271436254333400165270ustar00rootroot00000000000000package cli import ( "errors" "os" "strings" "github.com/traefik/paerser/file" "github.com/traefik/paerser/flag" "github.com/traefik/paerser/parser" ) // FileLoader loads a configuration from a file. type FileLoader struct { ConfigFileFlag string filename string BasePaths []string Extensions []string } // GetFilename returns the configuration file if any. func (f *FileLoader) GetFilename() string { return f.filename } // Load loads the command's configuration from a file either specified with the ConfigFileFlag flag, or from default locations. func (f *FileLoader) Load(args []string, cmd *Command) (bool, error) { ref, err := flag.Parse(args, cmd.Configuration) if err != nil { _ = cmd.PrintHelp(os.Stdout) return false, err } var configFileFlag string if f.ConfigFileFlag == "" { return false, errors.New("missing config file flag") } configFileFlag = parser.DefaultRootName + "." + f.ConfigFileFlag if _, ok := ref[strings.ToLower(configFileFlag)]; ok { configFileFlag = parser.DefaultRootName + "." + strings.ToLower(f.ConfigFileFlag) } configFile, err := f.loadConfigFiles(ref[configFileFlag], cmd.Configuration) if err != nil { return false, err } f.filename = configFile if configFile == "" { return false, nil } return true, nil } // loadConfigFiles tries to decode the given configuration file and all default locations for the configuration file. // It stops as soon as decoding one of them is successful. func (f *FileLoader) loadConfigFiles(configFile string, element interface{}) (string, error) { extensions := []string{"toml", "yaml", "yml"} if len(f.Extensions) != 0 { extensions = f.Extensions } if len(f.BasePaths) == 0 { return "", errors.New("missing base paths") } finder := Finder{ BasePaths: f.BasePaths, Extensions: extensions, } filePath, err := finder.Find(configFile) if err != nil { return "", err } if len(filePath) == 0 { return "", nil } if err = file.Decode(filePath, element); err != nil { return "", err } return filePath, nil } paerser-0.2.0/cli/loader_flag.go000066400000000000000000000007441436254333400165230ustar00rootroot00000000000000package cli import ( "fmt" "github.com/traefik/paerser/flag" ) // FlagLoader loads configuration from flags. type FlagLoader struct{} // Load loads the command's configuration from flag arguments. func (*FlagLoader) Load(args []string, cmd *Command) (bool, error) { if len(args) == 0 { return false, nil } if err := flag.Decode(args, cmd.Configuration); err != nil { return false, fmt.Errorf("failed to decode configuration from flags: %w", err) } return true, nil } paerser-0.2.0/env/000077500000000000000000000000001436254333400137515ustar00rootroot00000000000000paerser-0.2.0/env/env.go000066400000000000000000000050601436254333400150710ustar00rootroot00000000000000// Package env implements encoding and decoding between environment variable and a typed Configuration. package env import ( "fmt" "regexp" "strings" "github.com/traefik/paerser/parser" ) // DefaultNamePrefix is the default prefix for environment variable names. const DefaultNamePrefix = "TRAEFIK_" // Decode decodes the given environment variables into the given element. // The operation goes through four stages roughly summarized as: // - env vars -> map // - map -> tree of untyped nodes // - untyped nodes -> nodes augmented with metadata such as kind (inferred from element) // - "typed" nodes -> typed element. func Decode(environ []string, prefix string, element interface{}) error { if err := checkPrefix(prefix); err != nil { return err } vars := make(map[string]string) for _, evr := range environ { k, v, _ := strings.Cut(evr, "=") if strings.HasPrefix(strings.ToUpper(k), prefix) { key := strings.ReplaceAll(strings.ToLower(k), "_", ".") vars[key] = v } } rootName := strings.ToLower(prefix[:len(prefix)-1]) return parser.Decode(vars, element, rootName) } // Encode encodes the configuration in element into the environment variables represented in the returned Flats. // The operation goes through three stages roughly summarized as: // - typed configuration in element -> tree of untyped nodes // - untyped nodes -> nodes augmented with metadata such as kind (inferred from element) // - "typed" nodes -> environment variables with default values (determined by type/kind). func Encode(prefix string, element interface{}) ([]parser.Flat, error) { if err := checkPrefix(prefix); err != nil { return nil, err } rootName := strings.ToLower(prefix[:len(prefix)-1]) if element == nil { return nil, nil } etnOpts := parser.EncoderToNodeOpts{OmitEmpty: false, TagName: parser.TagLabel, AllowSliceAsStruct: true} node, err := parser.EncodeToNode(element, rootName, etnOpts) if err != nil { return nil, err } metaOpts := parser.MetadataOpts{TagName: parser.TagLabel, AllowSliceAsStruct: true} err = parser.AddMetadata(element, node, metaOpts) if err != nil { return nil, err } flatOpts := parser.FlatOpts{Case: "upper", Separator: "_", TagName: parser.TagLabel} return parser.EncodeToFlat(element, node, flatOpts) } func checkPrefix(prefix string) error { prefixPattern := `^[a-zA-Z0-9]+_$` matched, err := regexp.MatchString(prefixPattern, prefix) if err != nil { return err } if !matched { return fmt.Errorf("invalid prefix %q, the prefix pattern must match the following pattern: %s", prefix, prefixPattern) } return nil } paerser-0.2.0/env/env_test.go000066400000000000000000000224301436254333400161300ustar00rootroot00000000000000package env import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/traefik/paerser/generator" "github.com/traefik/paerser/parser" ) func TestDecode(t *testing.T) { testCases := []struct { desc string environ []string element interface{} expected interface{} }{ { desc: "no env vars", environ: nil, expected: nil, }, { desc: "bool value", environ: []string{"TRAEFIK_FOO=true"}, element: &struct { Foo bool }{}, expected: &struct { Foo bool }{ Foo: true, }, }, { desc: "equal", environ: []string{"TRAEFIK_FOO=bar"}, element: &struct { Foo string }{}, expected: &struct { Foo string }{ Foo: "bar", }, }, { desc: "multiple bool flags without value", environ: []string{"TRAEFIK_FOO=true", "TRAEFIK_BAR=true"}, element: &struct { Foo bool Bar bool }{}, expected: &struct { Foo bool Bar bool }{ Foo: true, Bar: true, }, }, { desc: "map string", environ: []string{"TRAEFIK_FOO_NAME=bar"}, element: &struct { Foo map[string]string }{}, expected: &struct { Foo map[string]string }{ Foo: map[string]string{ "name": "bar", }, }, }, { desc: "map struct", environ: []string{"TRAEFIK_FOO_NAME_VALUE=bar"}, element: &struct { Foo map[string]struct{ Value string } }{}, expected: &struct { Foo map[string]struct{ Value string } }{ Foo: map[string]struct{ Value string }{ "name": { Value: "bar", }, }, }, }, { desc: "map struct with sub-struct", environ: []string{"TRAEFIK_FOO_NAME_BAR_VALUE=bar"}, element: &struct { Foo map[string]struct { Bar *struct{ Value string } } }{}, expected: &struct { Foo map[string]struct { Bar *struct{ Value string } } }{ Foo: map[string]struct { Bar *struct{ Value string } }{ "name": { Bar: &struct { Value string }{ Value: "bar", }, }, }, }, }, { desc: "map struct with sub-map", environ: []string{"TRAEFIK_FOO_NAME1_BAR_NAME2_VALUE=bar"}, element: &struct { Foo map[string]struct { Bar map[string]struct{ Value string } } }{}, expected: &struct { Foo map[string]struct { Bar map[string]struct{ Value string } } }{ Foo: map[string]struct { Bar map[string]struct{ Value string } }{ "name1": { Bar: map[string]struct{ Value string }{ "name2": { Value: "bar", }, }, }, }, }, }, { desc: "slice", environ: []string{"TRAEFIK_FOO=bar,baz"}, element: &struct { Foo []string }{}, expected: &struct { Foo []string }{ Foo: []string{"bar", "baz"}, }, }, { desc: "struct pointer value", environ: []string{"TRAEFIK_FOO=true"}, element: &struct { Foo *struct{ Field string } `label:"allowEmpty"` }{}, expected: &struct { Foo *struct{ Field string } `label:"allowEmpty"` }{ Foo: &struct{ Field string }{}, }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() err := Decode(test.environ, DefaultNamePrefix, test.element) require.NoError(t, err) assert.Equal(t, test.expected, test.element) }) } } func TestEncode(t *testing.T) { element := &Ya{ Foo: &Yaa{ FieldIn1: "bar", FieldIn2: false, FieldIn3: 1, FieldIn4: map[string]string{ parser.MapNamePlaceholder: "", }, FieldIn5: map[string]int{ parser.MapNamePlaceholder: 0, }, FieldIn6: map[string]struct{ Field string }{ parser.MapNamePlaceholder: {}, }, FieldIn7: map[string]struct{ Field map[string]string }{ parser.MapNamePlaceholder: { Field: map[string]string{ parser.MapNamePlaceholder: "", }, }, }, FieldIn8: map[string]*struct{ Field string }{ parser.MapNamePlaceholder: {}, }, FieldIn9: map[string]*struct{ Field map[string]string }{ parser.MapNamePlaceholder: { Field: map[string]string{ parser.MapNamePlaceholder: "", }, }, }, FieldIn10: struct{ Field string }{}, FieldIn11: &struct{ Field string }{}, FieldIn12: func(v string) *string { return &v }(""), FieldIn13: func(v bool) *bool { return &v }(false), FieldIn14: func(v int) *int { return &v }(0), }, Field1: "bir", Field2: true, Field3: 0, Field4: map[string]string{ parser.MapNamePlaceholder: "", }, Field5: map[string]int{ parser.MapNamePlaceholder: 0, }, Field6: map[string]struct{ Field string }{ parser.MapNamePlaceholder: {}, }, Field7: map[string]struct{ Field map[string]string }{ parser.MapNamePlaceholder: { Field: map[string]string{ parser.MapNamePlaceholder: "", }, }, }, Field8: map[string]*struct{ Field string }{ parser.MapNamePlaceholder: {}, }, Field9: map[string]*struct{ Field map[string]string }{ parser.MapNamePlaceholder: { Field: map[string]string{ parser.MapNamePlaceholder: "", }, }, }, Field10: struct{ Field string }{}, Field11: &struct{ Field string }{}, Field12: func(v string) *string { return &v }(""), Field13: func(v bool) *bool { return &v }(false), Field14: func(v int) *int { return &v }(0), Field15: []int{7}, } generator.Generate(element) flats, err := Encode(DefaultNamePrefix, element) require.NoError(t, err) expected := []parser.Flat{ { Name: "TRAEFIK_FIELD1", Description: "", Default: "bir", }, { Name: "TRAEFIK_FIELD10", Description: "", Default: "", }, { Name: "TRAEFIK_FIELD10_FIELD", Description: "", Default: "", }, { Name: "TRAEFIK_FIELD11_FIELD", Description: "", Default: "", }, { Name: "TRAEFIK_FIELD12", Description: "", Default: "", }, { Name: "TRAEFIK_FIELD13", Description: "", Default: "false", }, { Name: "TRAEFIK_FIELD14", Description: "", Default: "0", }, { Name: "TRAEFIK_FIELD15", Description: "", Default: "7", }, { Name: "TRAEFIK_FIELD2", Description: "", Default: "true", }, { Name: "TRAEFIK_FIELD3", Description: "", Default: "0", }, { Name: "TRAEFIK_FIELD4_\u003cNAME\u003e", Description: "", Default: "", }, { Name: "TRAEFIK_FIELD5_\u003cNAME\u003e", Description: "", Default: "0", }, { Name: "TRAEFIK_FIELD6_\u003cNAME\u003e", Description: "", Default: "false", }, { Name: "TRAEFIK_FIELD6_\u003cNAME\u003e_FIELD", Description: "", Default: "", }, { Name: "TRAEFIK_FIELD7_\u003cNAME\u003e", Description: "", Default: "false", }, { Name: "TRAEFIK_FIELD7_\u003cNAME\u003e_FIELD_\u003cNAME\u003e", Description: "", Default: "", }, { Name: "TRAEFIK_FIELD8_\u003cNAME\u003e", Description: "", Default: "false", }, { Name: "TRAEFIK_FIELD8_\u003cNAME\u003e_FIELD", Description: "", Default: "", }, { Name: "TRAEFIK_FIELD9_\u003cNAME\u003e", Description: "", Default: "false", }, { Name: "TRAEFIK_FIELD9_\u003cNAME\u003e_FIELD_\u003cNAME\u003e", Description: "", Default: "", }, { Name: "TRAEFIK_FOO_FIELDIN1", Description: "", Default: "bar", }, { Name: "TRAEFIK_FOO_FIELDIN10", Description: "", Default: "", }, { Name: "TRAEFIK_FOO_FIELDIN10_FIELD", Description: "", Default: "", }, { Name: "TRAEFIK_FOO_FIELDIN11_FIELD", Description: "", Default: "", }, { Name: "TRAEFIK_FOO_FIELDIN12", Description: "", Default: "", }, { Name: "TRAEFIK_FOO_FIELDIN13", Description: "", Default: "false", }, { Name: "TRAEFIK_FOO_FIELDIN14", Description: "", Default: "0", }, { Name: "TRAEFIK_FOO_FIELDIN2", Description: "", Default: "false", }, { Name: "TRAEFIK_FOO_FIELDIN3", Description: "", Default: "1", }, { Name: "TRAEFIK_FOO_FIELDIN4_\u003cNAME\u003e", Description: "", Default: "", }, { Name: "TRAEFIK_FOO_FIELDIN5_\u003cNAME\u003e", Description: "", Default: "0", }, { Name: "TRAEFIK_FOO_FIELDIN6_\u003cNAME\u003e", Description: "", Default: "false", }, { Name: "TRAEFIK_FOO_FIELDIN6_\u003cNAME\u003e_FIELD", Description: "", Default: "", }, { Name: "TRAEFIK_FOO_FIELDIN7_\u003cNAME\u003e", Description: "", Default: "false", }, { Name: "TRAEFIK_FOO_FIELDIN7_\u003cNAME\u003e_FIELD_\u003cNAME\u003e", Description: "", Default: "", }, { Name: "TRAEFIK_FOO_FIELDIN8_\u003cNAME\u003e", Description: "", Default: "false", }, { Name: "TRAEFIK_FOO_FIELDIN8_\u003cNAME\u003e_FIELD", Description: "", Default: "", }, { Name: "TRAEFIK_FOO_FIELDIN9_\u003cNAME\u003e", Description: "", Default: "false", }, { Name: "TRAEFIK_FOO_FIELDIN9_\u003cNAME\u003e_FIELD_\u003cNAME\u003e", Description: "", Default: "", }, } assert.Equal(t, expected, flats) } paerser-0.2.0/env/filter.go000066400000000000000000000024361436254333400155720ustar00rootroot00000000000000package env import ( "reflect" "strings" "github.com/traefik/paerser/parser" ) // FindPrefixedEnvVars finds prefixed environment variables. func FindPrefixedEnvVars(environ []string, prefix string, element interface{}) []string { prefixes := getRootPrefixes(element, prefix) var values []string for _, px := range prefixes { for _, value := range environ { if strings.HasPrefix(value, px) { values = append(values, value) } } } return values } func getRootPrefixes(element interface{}, prefix string) []string { if element == nil { return nil } rootType := reflect.TypeOf(element) return getPrefixes(prefix, rootType) } func getPrefixes(prefix string, rootType reflect.Type) []string { var names []string if rootType.Kind() == reflect.Pointer { rootType = rootType.Elem() } if rootType.Kind() != reflect.Struct { return nil } for i := 0; i < rootType.NumField(); i++ { field := rootType.Field(i) if !parser.IsExported(field) { continue } if field.Anonymous && (field.Type.Kind() == reflect.Pointer && field.Type.Elem().Kind() == reflect.Struct || field.Type.Kind() == reflect.Struct) { names = append(names, getPrefixes(prefix, field.Type)...) continue } names = append(names, prefix+strings.ToUpper(field.Name)) } return names } paerser-0.2.0/env/filter_test.go000066400000000000000000000034651436254333400166340ustar00rootroot00000000000000package env import ( "testing" "github.com/stretchr/testify/assert" ) func TestFindPrefixedEnvVars(t *testing.T) { testCases := []struct { desc string environ []string element interface{} expected []string }{ { desc: "exact name", environ: []string{"TRAEFIK_FOO"}, element: &Yo{}, expected: []string{"TRAEFIK_FOO"}, }, { desc: "prefixed name", environ: []string{"TRAEFIK_FII01"}, element: &Yo{}, expected: []string{"TRAEFIK_FII01"}, }, { desc: "excluded env vars", environ: []string{"TRAEFIK_NOPE", "TRAEFIK_NO"}, element: &Yo{}, expected: nil, }, { desc: "filter", environ: []string{"TRAEFIK_NOPE", "TRAEFIK_NO", "TRAEFIK_FOO", "TRAEFIK_FII01"}, element: &Yo{}, expected: []string{"TRAEFIK_FOO", "TRAEFIK_FII01"}, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() vars := FindPrefixedEnvVars(test.environ, DefaultNamePrefix, test.element) assert.Equal(t, test.expected, vars) }) } } func Test_getRootFieldNames(t *testing.T) { testCases := []struct { desc string element interface{} expected []string }{ { desc: "simple fields", element: &Yo{}, expected: []string{"TRAEFIK_FOO", "TRAEFIK_FII", "TRAEFIK_FUU", "TRAEFIK_YI", "TRAEFIK_YU"}, }, { desc: "embedded struct", element: &Yu{}, expected: []string{"TRAEFIK_FOO", "TRAEFIK_FII", "TRAEFIK_FUU"}, }, { desc: "embedded struct pointer", element: &Ye{}, expected: []string{"TRAEFIK_FOO", "TRAEFIK_FII", "TRAEFIK_FUU"}, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() names := getRootPrefixes(test.element, DefaultNamePrefix) assert.Equal(t, test.expected, names) }) } } paerser-0.2.0/env/fixtures_test.go000066400000000000000000000024671436254333400172210ustar00rootroot00000000000000package env type Ya struct { Foo *Yaa Field1 string Field2 bool Field3 int Field4 map[string]string Field5 map[string]int Field6 map[string]struct{ Field string } Field7 map[string]struct{ Field map[string]string } Field8 map[string]*struct{ Field string } Field9 map[string]*struct{ Field map[string]string } Field10 struct{ Field string } Field11 *struct{ Field string } Field12 *string Field13 *bool Field14 *int Field15 []int } type Yaa struct { FieldIn1 string FieldIn2 bool FieldIn3 int FieldIn4 map[string]string FieldIn5 map[string]int FieldIn6 map[string]struct{ Field string } FieldIn7 map[string]struct{ Field map[string]string } FieldIn8 map[string]*struct{ Field string } FieldIn9 map[string]*struct{ Field map[string]string } FieldIn10 struct{ Field string } FieldIn11 *struct{ Field string } FieldIn12 *string FieldIn13 *bool FieldIn14 *int } type Yo struct { Foo string `description:"Foo description"` Fii string `description:"Fii description"` Fuu string `description:"Fuu description"` Yi *Yi `label:"allowEmpty"` Yu *Yi } func (y *Yo) SetDefaults() { y.Foo = "foo" y.Fii = "fii" } type Yi struct { Foo string Fii string Fuu string } func (y *Yi) SetDefaults() { y.Foo = "foo" y.Fii = "fii" } type Yu struct { Yi } type Ye struct { *Yi } paerser-0.2.0/file/000077500000000000000000000000001436254333400141005ustar00rootroot00000000000000paerser-0.2.0/file/file.go000066400000000000000000000044071436254333400153530ustar00rootroot00000000000000// Package file implements decoding between configuration in a file and a typed Configuration. package file import ( "fmt" "github.com/BurntSushi/toml" "github.com/traefik/paerser/parser" "gopkg.in/yaml.v3" ) const defaultRawSliceSeparator = "â•‘" // Decode decodes the given configuration file into the given element. // The operation goes through three stages roughly summarized as: // - file contents -> tree of untyped nodes // - untyped nodes -> nodes augmented with metadata such as kind (inferred from element) // - "typed" nodes -> typed element. func Decode(filePath string, element interface{}) error { if element == nil { return nil } filters := getRootFieldNames(element) root, err := decodeFileToNode(filePath, filters...) if err != nil { return err } metaOpts := parser.MetadataOpts{TagName: parser.TagFile, AllowSliceAsStruct: false} err = parser.AddMetadata(element, root, metaOpts) if err != nil { return err } return parser.Fill(element, root, parser.FillerOpts{AllowSliceAsStruct: false, RawSliceSeparator: defaultRawSliceSeparator}) } // DecodeContent decodes the given configuration file content into the given element. // The operation goes through three stages roughly summarized as: // - file contents -> tree of untyped nodes // - untyped nodes -> nodes augmented with metadata such as kind (inferred from element) // - "typed" nodes -> typed element. func DecodeContent(content, extension string, element interface{}) error { data := make(map[string]interface{}) switch extension { case ".toml": _, err := toml.Decode(content, &data) if err != nil { return err } case ".yml", ".yaml", ".json": var err error err = yaml.Unmarshal([]byte(content), &data) if err != nil { return err } default: return fmt.Errorf("unsupported file extension: %s", extension) } filters := getRootFieldNames(element) node, err := decodeRawToNode(data, filters...) if err != nil { return err } if len(node.Children) == 0 { return nil } metaOpts := parser.MetadataOpts{TagName: parser.TagFile, AllowSliceAsStruct: false} err = parser.AddMetadata(element, node, metaOpts) if err != nil { return err } return parser.Fill(element, node, parser.FillerOpts{AllowSliceAsStruct: false, RawSliceSeparator: defaultRawSliceSeparator}) } paerser-0.2.0/file/file_node.go000066400000000000000000000037171436254333400163630ustar00rootroot00000000000000package file import ( "fmt" "os" "path/filepath" "reflect" "strings" "github.com/BurntSushi/toml" "github.com/traefik/paerser/parser" "gopkg.in/yaml.v3" ) // decodeFileToNode decodes the configuration in filePath in a tree of untyped nodes. // If filters is not empty, it skips any configuration element whose name is not among filters. func decodeFileToNode(filePath string, filters ...string) (*parser.Node, error) { content, err := os.ReadFile(filepath.Clean(filePath)) if err != nil { return nil, err } data := make(map[string]interface{}) switch strings.ToLower(filepath.Ext(filePath)) { case ".toml": err = toml.Unmarshal(content, &data) if err != nil { return nil, err } case ".yml", ".yaml", ".json": err = yaml.Unmarshal(content, data) if err != nil { return nil, err } default: return nil, fmt.Errorf("unsupported file extension: %s", filePath) } if len(data) == 0 { return nil, fmt.Errorf("no configuration found in file: %s", filePath) } node, err := decodeRawToNode(data, filters...) if err != nil { return nil, err } if len(node.Children) == 0 { return nil, fmt.Errorf("no valid configuration found in file: %s", filePath) } return node, nil } func getRootFieldNames(element interface{}) []string { if element == nil { return nil } rootType := reflect.TypeOf(element) return getFieldNames(rootType) } func getFieldNames(rootType reflect.Type) []string { var names []string if rootType.Kind() == reflect.Pointer { rootType = rootType.Elem() } if rootType.Kind() != reflect.Struct { return nil } for i := 0; i < rootType.NumField(); i++ { field := rootType.Field(i) if !parser.IsExported(field) { continue } if field.Anonymous && (field.Type.Kind() == reflect.Pointer && field.Type.Elem().Kind() == reflect.Struct || field.Type.Kind() == reflect.Struct) { names = append(names, getFieldNames(field.Type)...) continue } names = append(names, field.Name) } return names } paerser-0.2.0/file/file_node_test.go000066400000000000000000001023001436254333400174060ustar00rootroot00000000000000package file import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/traefik/paerser/parser" ) func Test_getRootFieldNames(t *testing.T) { testCases := []struct { desc string element interface{} expected []string }{ { desc: "simple fields", element: &Yo{}, expected: []string{"Foo", "Fii", "Fuu", "Yi"}, }, { desc: "embedded struct", element: &Yu{}, expected: []string{"Foo", "Fii", "Fuu"}, }, { desc: "embedded struct pointer", element: &Ye{}, expected: []string{"Foo", "Fii", "Fuu"}, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() names := getRootFieldNames(test.element) assert.Equal(t, test.expected, names) }) } } func Test_decodeFileToNode_errors(t *testing.T) { testCases := []struct { desc string confFile string }{ { desc: "non existing file", confFile: "./fixtures/not_existing.toml", }, { desc: "file without content", confFile: "./fixtures/empty.toml", }, { desc: "file without any valid configuration", confFile: "./fixtures/no_conf.toml", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { node, err := decodeFileToNode(test.confFile, "Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "CertificatesResolvers") require.Error(t, err) assert.Nil(t, node) }) } } func Test_decodeFileToNode_compare(t *testing.T) { nodeToml, err := decodeFileToNode("./fixtures/sample.toml", "Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "CertificatesResolvers") require.NoError(t, err) nodeYaml, err := decodeFileToNode("./fixtures/sample.yml") require.NoError(t, err) assert.Equal(t, nodeToml, nodeYaml) } func Test_decodeFileToNode_Toml(t *testing.T) { node, err := decodeFileToNode("./fixtures/sample.toml", "Global", "ServersTransport", "EntryPoints", "Providers", "API", "Metrics", "Ping", "Log", "AccessLog", "Tracing", "HostResolver", "CertificatesResolvers") require.NoError(t, err) expected := &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "accessLog", Children: []*parser.Node{ {Name: "bufferingSize", Value: "42"}, {Name: "fields", Children: []*parser.Node{ {Name: "defaultMode", Value: "foobar"}, {Name: "headers", Children: []*parser.Node{ {Name: "defaultMode", Value: "foobar"}, {Name: "names", Children: []*parser.Node{ {Name: "name0", Value: "foobar"}, {Name: "name1", Value: "foobar"}, }}, }}, {Name: "names", Children: []*parser.Node{ {Name: "name0", Value: "foobar"}, {Name: "name1", Value: "foobar"}, }}, }}, {Name: "filePath", Value: "foobar"}, {Name: "filters", Children: []*parser.Node{ {Name: "minDuration", Value: "42"}, {Name: "retryAttempts", Value: "true"}, {Name: "statusCodes", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "format", Value: "foobar"}, }}, {Name: "api", Children: []*parser.Node{ {Name: "dashboard", Value: "true"}, {Name: "entryPoint", Value: "foobar"}, {Name: "middlewares", Value: "â•‘24â•‘foobarâ•‘foobar"}, {Name: "statistics", Children: []*parser.Node{ {Name: "recentErrors", Value: "42"}, }}, }}, {Name: "certificatesResolvers", Children: []*parser.Node{ {Name: "default", Children: []*parser.Node{ { Name: "acme", Children: []*parser.Node{ {Name: "acmeLogging", Value: "true"}, {Name: "caServer", Value: "foobar"}, {Name: "dnsChallenge", Children: []*parser.Node{ {Name: "delayBeforeCheck", Value: "42"}, {Name: "disablePropagationCheck", Value: "true"}, {Name: "provider", Value: "foobar"}, {Name: "resolvers", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "email", Value: "foobar"}, {Name: "entryPoint", Value: "foobar"}, {Name: "httpChallenge", Children: []*parser.Node{ {Name: "entryPoint", Value: "foobar"}, }}, {Name: "keyType", Value: "foobar"}, {Name: "storage", Value: "foobar"}, {Name: "tlsChallenge"}, }, }, }}, }}, {Name: "entryPoints", Children: []*parser.Node{ {Name: "EntryPoint0", Children: []*parser.Node{ {Name: "address", Value: "foobar"}, {Name: "forwardedHeaders", Children: []*parser.Node{ {Name: "insecure", Value: "true"}, {Name: "trustedIPs", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "proxyProtocol", Children: []*parser.Node{ {Name: "insecure", Value: "true"}, {Name: "trustedIPs", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "transport", Children: []*parser.Node{ {Name: "lifeCycle", Children: []*parser.Node{ {Name: "graceTimeOut", Value: "42"}, {Name: "requestAcceptGraceTimeout", Value: "42"}, }}, {Name: "respondingTimeouts", Children: []*parser.Node{ {Name: "idleTimeout", Value: "42"}, {Name: "readTimeout", Value: "42"}, {Name: "writeTimeout", Value: "42"}, }}, }}, }}, }}, {Name: "global", Children: []*parser.Node{ {Name: "checkNewVersion", Value: "true"}, {Name: "sendAnonymousUsage", Value: "true"}, }}, {Name: "hostResolver", Children: []*parser.Node{ {Name: "cnameFlattening", Value: "true"}, {Name: "resolvConfig", Value: "foobar"}, {Name: "resolvDepth", Value: "42"}, }}, {Name: "log", Children: []*parser.Node{ {Name: "filePath", Value: "foobar"}, {Name: "format", Value: "foobar"}, {Name: "level", Value: "foobar"}, }}, {Name: "metrics", Children: []*parser.Node{ {Name: "datadog", Children: []*parser.Node{ {Name: "address", Value: "foobar"}, {Name: "pushInterval", Value: "10s"}, }}, {Name: "influxDB", Children: []*parser.Node{ {Name: "address", Value: "foobar"}, {Name: "database", Value: "foobar"}, {Name: "password", Value: "foobar"}, {Name: "protocol", Value: "foobar"}, {Name: "pushInterval", Value: "10s"}, {Name: "retentionPolicy", Value: "foobar"}, {Name: "username", Value: "foobar"}, }}, {Name: "prometheus", Children: []*parser.Node{ {Name: "buckets", Value: "â•‘14â•‘42.010000â•‘42.020000"}, {Name: "entryPoint", Value: "foobar"}, {Name: "middlewares", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "statsD", Children: []*parser.Node{ {Name: "address", Value: "foobar"}, {Name: "pushInterval", Value: "10s"}, }}, }}, {Name: "ping", Children: []*parser.Node{ {Name: "entryPoint", Value: "foobar"}, {Name: "middlewares", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "providers", Children: []*parser.Node{ {Name: "docker", Children: []*parser.Node{ {Name: "constraints", Value: "foobar"}, {Name: "defaultRule", Value: "foobar"}, {Name: "endpoint", Value: "foobar"}, {Name: "exposedByDefault", Value: "true"}, {Name: "network", Value: "foobar"}, {Name: "swarmMode", Value: "true"}, {Name: "swarmModeRefreshSeconds", Value: "42"}, {Name: "tls", Children: []*parser.Node{ {Name: "ca", Value: "foobar"}, {Name: "caOptional", Value: "true"}, {Name: "cert", Value: "foobar"}, {Name: "insecureSkipVerify", Value: "true"}, {Name: "key", Value: "foobar"}, }}, {Name: "useBindPortIP", Value: "true"}, {Name: "watch", Value: "true"}, }}, {Name: "file", Children: []*parser.Node{ {Name: "debugLogGeneratedTemplate", Value: "true"}, {Name: "directory", Value: "foobar"}, {Name: "filename", Value: "foobar"}, {Name: "watch", Value: "true"}, }}, { Name: "kubernetesCRD", Children: []*parser.Node{ {Name: "certAuthFilePath", Value: "foobar"}, {Name: "disablePassHostHeaders", Value: "true"}, {Name: "endpoint", Value: "foobar"}, {Name: "ingressClass", Value: "foobar"}, {Name: "labelSelector", Value: "foobar"}, {Name: "namespaces", Value: "â•‘24â•‘foobarâ•‘foobar"}, {Name: "token", Value: "foobar"}, }, }, {Name: "kubernetesIngress", Children: []*parser.Node{ {Name: "certAuthFilePath", Value: "foobar"}, {Name: "disablePassHostHeaders", Value: "true"}, {Name: "endpoint", Value: "foobar"}, {Name: "ingressClass", Value: "foobar"}, {Name: "ingressEndpoint", Children: []*parser.Node{ {Name: "hostname", Value: "foobar"}, {Name: "ip", Value: "foobar"}, {Name: "publishedService", Value: "foobar"}, }}, {Name: "labelSelector", Value: "foobar"}, {Name: "namespaces", Value: "â•‘24â•‘foobarâ•‘foobar"}, {Name: "token", Value: "foobar"}, }}, {Name: "marathon", Children: []*parser.Node{ {Name: "basic", Children: []*parser.Node{ {Name: "httpBasicAuthUser", Value: "foobar"}, {Name: "httpBasicPassword", Value: "foobar"}, }}, {Name: "constraints", Value: "foobar"}, {Name: "dcosToken", Value: "foobar"}, {Name: "defaultRule", Value: "foobar"}, {Name: "dialerTimeout", Value: "42"}, {Name: "endpoint", Value: "foobar"}, {Name: "exposedByDefault", Value: "true"}, {Name: "forceTaskHostname", Value: "true"}, {Name: "keepAlive", Value: "42"}, {Name: "respectReadinessChecks", Value: "true"}, {Name: "responseHeaderTimeout", Value: "42"}, {Name: "tls", Children: []*parser.Node{ {Name: "ca", Value: "foobar"}, {Name: "caOptional", Value: "true"}, {Name: "cert", Value: "foobar"}, {Name: "insecureSkipVerify", Value: "true"}, {Name: "key", Value: "foobar"}, }}, {Name: "tlsHandshakeTimeout", Value: "42"}, {Name: "trace", Value: "true"}, {Name: "watch", Value: "true"}, }}, {Name: "providersThrottleDuration", Value: "42"}, {Name: "rancher", Children: []*parser.Node{ {Name: "constraints", Value: "foobar"}, {Name: "defaultRule", Value: "foobar"}, {Name: "enableServiceHealthFilter", Value: "true"}, {Name: "exposedByDefault", Value: "true"}, {Name: "intervalPoll", Value: "true"}, {Name: "prefix", Value: "foobar"}, {Name: "refreshSeconds", Value: "42"}, {Name: "watch", Value: "true"}, }}, {Name: "rest", Children: []*parser.Node{ {Name: "entryPoint", Value: "foobar"}, }}, }}, {Name: "serversTransport", Children: []*parser.Node{ {Name: "forwardingTimeouts", Children: []*parser.Node{ {Name: "dialTimeout", Value: "42"}, {Name: "idleConnTimeout", Value: "42"}, {Name: "responseHeaderTimeout", Value: "42"}, }}, {Name: "insecureSkipVerify", Value: "true"}, {Name: "maxIdleConnsPerHost", Value: "42"}, {Name: "rootCAs", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "tracing", Children: []*parser.Node{ {Name: "datadog", Children: []*parser.Node{ {Name: "bagagePrefixHeaderName", Value: "foobar"}, {Name: "debug", Value: "true"}, {Name: "globalTag", Value: "foobar"}, {Name: "localAgentHostPort", Value: "foobar"}, {Name: "parentIDHeaderName", Value: "foobar"}, {Name: "prioritySampling", Value: "true"}, {Name: "samplingPriorityHeaderName", Value: "foobar"}, {Name: "traceIDHeaderName", Value: "foobar"}, }}, {Name: "haystack", Children: []*parser.Node{ {Name: "globalTag", Value: "foobar"}, {Name: "localAgentHost", Value: "foobar"}, {Name: "localAgentPort", Value: "42"}, {Name: "parentIDHeaderName", Value: "foobar"}, {Name: "spanIDHeaderName", Value: "foobar"}, {Name: "traceIDHeaderName", Value: "foobar"}, }}, {Name: "instana", Children: []*parser.Node{ {Name: "localAgentHost", Value: "foobar"}, {Name: "localAgentPort", Value: "42"}, {Name: "logLevel", Value: "foobar"}, }}, {Name: "jaeger", Children: []*parser.Node{ {Name: "gen128Bit", Value: "true"}, {Name: "localAgentHostPort", Value: "foobar"}, {Name: "propagation", Value: "foobar"}, {Name: "samplingParam", Value: "42"}, {Name: "samplingServerURL", Value: "foobar"}, {Name: "samplingType", Value: "foobar"}, {Name: "traceContextHeaderName", Value: "foobar"}, }}, {Name: "serviceName", Value: "foobar"}, {Name: "spanNameLimit", Value: "42"}, {Name: "zipkin", Children: []*parser.Node{ {Name: "httpEndpoint", Value: "foobar"}, {Name: "id128Bit", Value: "true"}, {Name: "sameSpan", Value: "true"}, {Name: "sampleRate", Value: "42"}, }}, }}, }, } assert.Equal(t, expected, node) } func Test_decodeFileToNode_Yaml(t *testing.T) { node, err := decodeFileToNode("./fixtures/sample.yml") require.NoError(t, err) expected := &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "accessLog", Children: []*parser.Node{ {Name: "bufferingSize", Value: "42"}, {Name: "fields", Children: []*parser.Node{ {Name: "defaultMode", Value: "foobar"}, {Name: "headers", Children: []*parser.Node{ {Name: "defaultMode", Value: "foobar"}, {Name: "names", Children: []*parser.Node{ {Name: "name0", Value: "foobar"}, {Name: "name1", Value: "foobar"}, }}, }}, {Name: "names", Children: []*parser.Node{ {Name: "name0", Value: "foobar"}, {Name: "name1", Value: "foobar"}, }}, }}, {Name: "filePath", Value: "foobar"}, {Name: "filters", Children: []*parser.Node{ {Name: "minDuration", Value: "42"}, {Name: "retryAttempts", Value: "true"}, {Name: "statusCodes", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "format", Value: "foobar"}, }}, {Name: "api", Children: []*parser.Node{ {Name: "dashboard", Value: "true"}, {Name: "entryPoint", Value: "foobar"}, {Name: "middlewares", Value: "â•‘24â•‘foobarâ•‘foobar"}, {Name: "statistics", Children: []*parser.Node{ {Name: "recentErrors", Value: "42"}, }}, }}, {Name: "certificatesResolvers", Children: []*parser.Node{ {Name: "default", Children: []*parser.Node{ { Name: "acme", Children: []*parser.Node{ {Name: "acmeLogging", Value: "true"}, {Name: "caServer", Value: "foobar"}, {Name: "dnsChallenge", Children: []*parser.Node{ {Name: "delayBeforeCheck", Value: "42"}, {Name: "disablePropagationCheck", Value: "true"}, {Name: "provider", Value: "foobar"}, {Name: "resolvers", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "email", Value: "foobar"}, {Name: "entryPoint", Value: "foobar"}, {Name: "httpChallenge", Children: []*parser.Node{ {Name: "entryPoint", Value: "foobar"}, }}, {Name: "keyType", Value: "foobar"}, {Name: "storage", Value: "foobar"}, {Name: "tlsChallenge"}, }, }, }}, }}, {Name: "entryPoints", Children: []*parser.Node{ {Name: "EntryPoint0", Children: []*parser.Node{ {Name: "address", Value: "foobar"}, {Name: "forwardedHeaders", Children: []*parser.Node{ {Name: "insecure", Value: "true"}, {Name: "trustedIPs", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "proxyProtocol", Children: []*parser.Node{ {Name: "insecure", Value: "true"}, {Name: "trustedIPs", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "transport", Children: []*parser.Node{ {Name: "lifeCycle", Children: []*parser.Node{ {Name: "graceTimeOut", Value: "42"}, {Name: "requestAcceptGraceTimeout", Value: "42"}, }}, {Name: "respondingTimeouts", Children: []*parser.Node{ {Name: "idleTimeout", Value: "42"}, {Name: "readTimeout", Value: "42"}, {Name: "writeTimeout", Value: "42"}, }}, }}, }}, }}, {Name: "global", Children: []*parser.Node{ {Name: "checkNewVersion", Value: "true"}, {Name: "sendAnonymousUsage", Value: "true"}, }}, {Name: "hostResolver", Children: []*parser.Node{ {Name: "cnameFlattening", Value: "true"}, {Name: "resolvConfig", Value: "foobar"}, {Name: "resolvDepth", Value: "42"}, }}, {Name: "log", Children: []*parser.Node{ {Name: "filePath", Value: "foobar"}, {Name: "format", Value: "foobar"}, {Name: "level", Value: "foobar"}, }}, {Name: "metrics", Children: []*parser.Node{ {Name: "datadog", Children: []*parser.Node{ {Name: "address", Value: "foobar"}, {Name: "pushInterval", Value: "10s"}, }}, {Name: "influxDB", Children: []*parser.Node{ {Name: "address", Value: "foobar"}, {Name: "database", Value: "foobar"}, {Name: "password", Value: "foobar"}, {Name: "protocol", Value: "foobar"}, {Name: "pushInterval", Value: "10s"}, {Name: "retentionPolicy", Value: "foobar"}, {Name: "username", Value: "foobar"}, }}, {Name: "prometheus", Children: []*parser.Node{ {Name: "buckets", Value: "â•‘14â•‘42.010000â•‘42.020000"}, {Name: "entryPoint", Value: "foobar"}, {Name: "middlewares", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "statsD", Children: []*parser.Node{ {Name: "address", Value: "foobar"}, {Name: "pushInterval", Value: "10s"}, }}, }}, {Name: "ping", Children: []*parser.Node{ {Name: "entryPoint", Value: "foobar"}, {Name: "middlewares", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "providers", Children: []*parser.Node{ {Name: "docker", Children: []*parser.Node{ {Name: "constraints", Value: "foobar"}, {Name: "defaultRule", Value: "foobar"}, {Name: "endpoint", Value: "foobar"}, {Name: "exposedByDefault", Value: "true"}, {Name: "network", Value: "foobar"}, {Name: "swarmMode", Value: "true"}, {Name: "swarmModeRefreshSeconds", Value: "42"}, {Name: "tls", Children: []*parser.Node{ {Name: "ca", Value: "foobar"}, {Name: "caOptional", Value: "true"}, {Name: "cert", Value: "foobar"}, {Name: "insecureSkipVerify", Value: "true"}, {Name: "key", Value: "foobar"}, }}, {Name: "useBindPortIP", Value: "true"}, {Name: "watch", Value: "true"}, }}, {Name: "file", Children: []*parser.Node{ {Name: "debugLogGeneratedTemplate", Value: "true"}, {Name: "directory", Value: "foobar"}, {Name: "filename", Value: "foobar"}, {Name: "watch", Value: "true"}, }}, { Name: "kubernetesCRD", Children: []*parser.Node{ {Name: "certAuthFilePath", Value: "foobar"}, {Name: "disablePassHostHeaders", Value: "true"}, {Name: "endpoint", Value: "foobar"}, {Name: "ingressClass", Value: "foobar"}, {Name: "labelSelector", Value: "foobar"}, {Name: "namespaces", Value: "â•‘24â•‘foobarâ•‘foobar"}, {Name: "token", Value: "foobar"}, }, }, {Name: "kubernetesIngress", Children: []*parser.Node{ {Name: "certAuthFilePath", Value: "foobar"}, {Name: "disablePassHostHeaders", Value: "true"}, {Name: "endpoint", Value: "foobar"}, {Name: "ingressClass", Value: "foobar"}, {Name: "ingressEndpoint", Children: []*parser.Node{ {Name: "hostname", Value: "foobar"}, {Name: "ip", Value: "foobar"}, {Name: "publishedService", Value: "foobar"}, }}, {Name: "labelSelector", Value: "foobar"}, {Name: "namespaces", Value: "â•‘24â•‘foobarâ•‘foobar"}, {Name: "token", Value: "foobar"}, }}, {Name: "marathon", Children: []*parser.Node{ {Name: "basic", Children: []*parser.Node{ {Name: "httpBasicAuthUser", Value: "foobar"}, {Name: "httpBasicPassword", Value: "foobar"}, }}, {Name: "constraints", Value: "foobar"}, {Name: "dcosToken", Value: "foobar"}, {Name: "defaultRule", Value: "foobar"}, {Name: "dialerTimeout", Value: "42"}, {Name: "endpoint", Value: "foobar"}, {Name: "exposedByDefault", Value: "true"}, {Name: "forceTaskHostname", Value: "true"}, {Name: "keepAlive", Value: "42"}, {Name: "respectReadinessChecks", Value: "true"}, {Name: "responseHeaderTimeout", Value: "42"}, {Name: "tls", Children: []*parser.Node{ {Name: "ca", Value: "foobar"}, {Name: "caOptional", Value: "true"}, {Name: "cert", Value: "foobar"}, {Name: "insecureSkipVerify", Value: "true"}, {Name: "key", Value: "foobar"}, }}, {Name: "tlsHandshakeTimeout", Value: "42"}, {Name: "trace", Value: "true"}, {Name: "watch", Value: "true"}, }}, {Name: "providersThrottleDuration", Value: "42"}, {Name: "rancher", Children: []*parser.Node{ {Name: "constraints", Value: "foobar"}, {Name: "defaultRule", Value: "foobar"}, {Name: "enableServiceHealthFilter", Value: "true"}, {Name: "exposedByDefault", Value: "true"}, {Name: "intervalPoll", Value: "true"}, {Name: "prefix", Value: "foobar"}, {Name: "refreshSeconds", Value: "42"}, {Name: "watch", Value: "true"}, }}, {Name: "rest", Children: []*parser.Node{ {Name: "entryPoint", Value: "foobar"}, }}, }}, {Name: "serversTransport", Children: []*parser.Node{ {Name: "forwardingTimeouts", Children: []*parser.Node{ {Name: "dialTimeout", Value: "42"}, {Name: "idleConnTimeout", Value: "42"}, {Name: "responseHeaderTimeout", Value: "42"}, }}, {Name: "insecureSkipVerify", Value: "true"}, {Name: "maxIdleConnsPerHost", Value: "42"}, {Name: "rootCAs", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "tracing", Children: []*parser.Node{ {Name: "datadog", Children: []*parser.Node{ {Name: "bagagePrefixHeaderName", Value: "foobar"}, {Name: "debug", Value: "true"}, {Name: "globalTag", Value: "foobar"}, {Name: "localAgentHostPort", Value: "foobar"}, {Name: "parentIDHeaderName", Value: "foobar"}, {Name: "prioritySampling", Value: "true"}, {Name: "samplingPriorityHeaderName", Value: "foobar"}, {Name: "traceIDHeaderName", Value: "foobar"}, }}, {Name: "haystack", Children: []*parser.Node{ {Name: "globalTag", Value: "foobar"}, {Name: "localAgentHost", Value: "foobar"}, {Name: "localAgentPort", Value: "42"}, {Name: "parentIDHeaderName", Value: "foobar"}, {Name: "spanIDHeaderName", Value: "foobar"}, {Name: "traceIDHeaderName", Value: "foobar"}, }}, {Name: "instana", Children: []*parser.Node{ {Name: "localAgentHost", Value: "foobar"}, {Name: "localAgentPort", Value: "42"}, {Name: "logLevel", Value: "foobar"}, }}, {Name: "jaeger", Children: []*parser.Node{ {Name: "gen128Bit", Value: "true"}, {Name: "localAgentHostPort", Value: "foobar"}, {Name: "propagation", Value: "foobar"}, {Name: "samplingParam", Value: "42"}, {Name: "samplingServerURL", Value: "foobar"}, {Name: "samplingType", Value: "foobar"}, {Name: "traceContextHeaderName", Value: "foobar"}, }}, {Name: "serviceName", Value: "foobar"}, {Name: "spanNameLimit", Value: "42"}, {Name: "zipkin", Children: []*parser.Node{ {Name: "httpEndpoint", Value: "foobar"}, {Name: "id128Bit", Value: "true"}, {Name: "sameSpan", Value: "true"}, {Name: "sampleRate", Value: "42"}, }}, }}, }, } assert.Equal(t, expected, node) } func Test_decodeFileToNode_Json(t *testing.T) { node, err := decodeFileToNode("./fixtures/sample.json") require.NoError(t, err) expected := &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "accessLog", Children: []*parser.Node{ {Name: "bufferingSize", Value: "42"}, {Name: "fields", Children: []*parser.Node{ {Name: "defaultMode", Value: "foobar"}, {Name: "headers", Children: []*parser.Node{ {Name: "defaultMode", Value: "foobar"}, {Name: "names", Children: []*parser.Node{ {Name: "name0", Value: "foobar"}, {Name: "name1", Value: "foobar"}, }}, }}, {Name: "names", Children: []*parser.Node{ {Name: "name0", Value: "foobar"}, {Name: "name1", Value: "foobar"}, }}, }}, {Name: "filePath", Value: "foobar"}, {Name: "filters", Children: []*parser.Node{ {Name: "minDuration", Value: "42"}, {Name: "retryAttempts", Value: "true"}, {Name: "statusCodes", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "format", Value: "foobar"}, }}, {Name: "api", Children: []*parser.Node{ {Name: "dashboard", Value: "true"}, {Name: "entryPoint", Value: "foobar"}, {Name: "middlewares", Value: "â•‘24â•‘foobarâ•‘foobar"}, {Name: "statistics", Children: []*parser.Node{ {Name: "recentErrors", Value: "42"}, }}, }}, {Name: "certificatesResolvers", Children: []*parser.Node{ {Name: "default", Children: []*parser.Node{ { Name: "acme", Children: []*parser.Node{ {Name: "acmeLogging", Value: "true"}, {Name: "caServer", Value: "foobar"}, {Name: "dnsChallenge", Children: []*parser.Node{ {Name: "delayBeforeCheck", Value: "42"}, {Name: "disablePropagationCheck", Value: "true"}, {Name: "provider", Value: "foobar"}, {Name: "resolvers", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "email", Value: "foobar"}, {Name: "entryPoint", Value: "foobar"}, {Name: "httpChallenge", Children: []*parser.Node{ {Name: "entryPoint", Value: "foobar"}, }}, {Name: "keyType", Value: "foobar"}, {Name: "storage", Value: "foobar"}, {Name: "tlsChallenge"}, }, }, }}, }}, {Name: "entryPoints", Children: []*parser.Node{ {Name: "EntryPoint0", Children: []*parser.Node{ {Name: "address", Value: "foobar"}, {Name: "forwardedHeaders", Children: []*parser.Node{ {Name: "insecure", Value: "true"}, {Name: "trustedIPs", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "proxyProtocol", Children: []*parser.Node{ {Name: "insecure", Value: "true"}, {Name: "trustedIPs", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "transport", Children: []*parser.Node{ {Name: "lifeCycle", Children: []*parser.Node{ {Name: "graceTimeOut", Value: "42"}, {Name: "requestAcceptGraceTimeout", Value: "42"}, }}, {Name: "respondingTimeouts", Children: []*parser.Node{ {Name: "idleTimeout", Value: "42"}, {Name: "readTimeout", Value: "42"}, {Name: "writeTimeout", Value: "42"}, }}, }}, }}, }}, {Name: "global", Children: []*parser.Node{ {Name: "checkNewVersion", Value: "true"}, {Name: "sendAnonymousUsage", Value: "true"}, }}, {Name: "hostResolver", Children: []*parser.Node{ {Name: "cnameFlattening", Value: "true"}, {Name: "resolvConfig", Value: "foobar"}, {Name: "resolvDepth", Value: "42"}, }}, {Name: "log", Children: []*parser.Node{ {Name: "filePath", Value: "foobar"}, {Name: "format", Value: "foobar"}, {Name: "level", Value: "foobar"}, }}, {Name: "metrics", Children: []*parser.Node{ {Name: "datadog", Children: []*parser.Node{ {Name: "address", Value: "foobar"}, {Name: "pushInterval", Value: "10s"}, }}, {Name: "influxDB", Children: []*parser.Node{ {Name: "address", Value: "foobar"}, {Name: "database", Value: "foobar"}, {Name: "password", Value: "foobar"}, {Name: "protocol", Value: "foobar"}, {Name: "pushInterval", Value: "10s"}, {Name: "retentionPolicy", Value: "foobar"}, {Name: "username", Value: "foobar"}, }}, {Name: "prometheus", Children: []*parser.Node{ {Name: "buckets", Value: "â•‘14â•‘42.010000â•‘42.020000"}, {Name: "entryPoint", Value: "foobar"}, {Name: "middlewares", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "statsD", Children: []*parser.Node{ {Name: "address", Value: "foobar"}, {Name: "pushInterval", Value: "10s"}, }}, }}, {Name: "ping", Children: []*parser.Node{ {Name: "entryPoint", Value: "foobar"}, {Name: "middlewares", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "providers", Children: []*parser.Node{ {Name: "docker", Children: []*parser.Node{ {Name: "constraints", Value: "foobar"}, {Name: "defaultRule", Value: "foobar"}, {Name: "endpoint", Value: "foobar"}, {Name: "exposedByDefault", Value: "true"}, {Name: "network", Value: "foobar"}, {Name: "swarmMode", Value: "true"}, {Name: "swarmModeRefreshSeconds", Value: "42"}, {Name: "tls", Children: []*parser.Node{ {Name: "ca", Value: "foobar"}, {Name: "caOptional", Value: "true"}, {Name: "cert", Value: "foobar"}, {Name: "insecureSkipVerify", Value: "true"}, {Name: "key", Value: "foobar"}, }}, {Name: "useBindPortIP", Value: "true"}, {Name: "watch", Value: "true"}, }}, {Name: "file", Children: []*parser.Node{ {Name: "debugLogGeneratedTemplate", Value: "true"}, {Name: "directory", Value: "foobar"}, {Name: "filename", Value: "foobar"}, {Name: "watch", Value: "true"}, }}, { Name: "kubernetesCRD", Children: []*parser.Node{ {Name: "certAuthFilePath", Value: "foobar"}, {Name: "disablePassHostHeaders", Value: "true"}, {Name: "endpoint", Value: "foobar"}, {Name: "ingressClass", Value: "foobar"}, {Name: "labelSelector", Value: "foobar"}, {Name: "namespaces", Value: "â•‘24â•‘foobarâ•‘foobar"}, {Name: "token", Value: "foobar"}, }, }, {Name: "kubernetesIngress", Children: []*parser.Node{ {Name: "certAuthFilePath", Value: "foobar"}, {Name: "disablePassHostHeaders", Value: "true"}, {Name: "endpoint", Value: "foobar"}, {Name: "ingressClass", Value: "foobar"}, {Name: "ingressEndpoint", Children: []*parser.Node{ {Name: "hostname", Value: "foobar"}, {Name: "ip", Value: "foobar"}, {Name: "publishedService", Value: "foobar"}, }}, {Name: "labelSelector", Value: "foobar"}, {Name: "namespaces", Value: "â•‘24â•‘foobarâ•‘foobar"}, {Name: "token", Value: "foobar"}, }}, {Name: "marathon", Children: []*parser.Node{ {Name: "basic", Children: []*parser.Node{ {Name: "httpBasicAuthUser", Value: "foobar"}, {Name: "httpBasicPassword", Value: "foobar"}, }}, {Name: "constraints", Value: "foobar"}, {Name: "dcosToken", Value: "foobar"}, {Name: "defaultRule", Value: "foobar"}, {Name: "dialerTimeout", Value: "42"}, {Name: "endpoint", Value: "foobar"}, {Name: "exposedByDefault", Value: "true"}, {Name: "forceTaskHostname", Value: "true"}, {Name: "keepAlive", Value: "42"}, {Name: "respectReadinessChecks", Value: "true"}, {Name: "responseHeaderTimeout", Value: "42"}, {Name: "tls", Children: []*parser.Node{ {Name: "ca", Value: "foobar"}, {Name: "caOptional", Value: "true"}, {Name: "cert", Value: "foobar"}, {Name: "insecureSkipVerify", Value: "true"}, {Name: "key", Value: "foobar"}, }}, {Name: "tlsHandshakeTimeout", Value: "42"}, {Name: "trace", Value: "true"}, {Name: "watch", Value: "true"}, }}, {Name: "providersThrottleDuration", Value: "42"}, {Name: "rancher", Children: []*parser.Node{ {Name: "constraints", Value: "foobar"}, {Name: "defaultRule", Value: "foobar"}, {Name: "enableServiceHealthFilter", Value: "true"}, {Name: "exposedByDefault", Value: "true"}, {Name: "intervalPoll", Value: "true"}, {Name: "prefix", Value: "foobar"}, {Name: "refreshSeconds", Value: "42"}, {Name: "watch", Value: "true"}, }}, {Name: "rest", Children: []*parser.Node{ {Name: "entryPoint", Value: "foobar"}, }}, }}, {Name: "serversTransport", Children: []*parser.Node{ {Name: "forwardingTimeouts", Children: []*parser.Node{ {Name: "dialTimeout", Value: "42"}, {Name: "idleConnTimeout", Value: "42"}, {Name: "responseHeaderTimeout", Value: "42"}, }}, {Name: "insecureSkipVerify", Value: "true"}, {Name: "maxIdleConnsPerHost", Value: "42"}, {Name: "rootCAs", Value: "â•‘24â•‘foobarâ•‘foobar"}, }}, {Name: "tracing", Children: []*parser.Node{ {Name: "datadog", Children: []*parser.Node{ {Name: "bagagePrefixHeaderName", Value: "foobar"}, {Name: "debug", Value: "true"}, {Name: "globalTag", Value: "foobar"}, {Name: "localAgentHostPort", Value: "foobar"}, {Name: "parentIDHeaderName", Value: "foobar"}, {Name: "prioritySampling", Value: "true"}, {Name: "samplingPriorityHeaderName", Value: "foobar"}, {Name: "traceIDHeaderName", Value: "foobar"}, }}, {Name: "haystack", Children: []*parser.Node{ {Name: "globalTag", Value: "foobar"}, {Name: "localAgentHost", Value: "foobar"}, {Name: "localAgentPort", Value: "42"}, {Name: "parentIDHeaderName", Value: "foobar"}, {Name: "spanIDHeaderName", Value: "foobar"}, {Name: "traceIDHeaderName", Value: "foobar"}, }}, {Name: "instana", Children: []*parser.Node{ {Name: "localAgentHost", Value: "foobar"}, {Name: "localAgentPort", Value: "42"}, {Name: "logLevel", Value: "foobar"}, }}, {Name: "jaeger", Children: []*parser.Node{ {Name: "gen128Bit", Value: "true"}, {Name: "localAgentHostPort", Value: "foobar"}, {Name: "propagation", Value: "foobar"}, {Name: "samplingParam", Value: "42"}, {Name: "samplingServerURL", Value: "foobar"}, {Name: "samplingType", Value: "foobar"}, {Name: "traceContextHeaderName", Value: "foobar"}, }}, {Name: "serviceName", Value: "foobar"}, {Name: "spanNameLimit", Value: "42"}, {Name: "zipkin", Children: []*parser.Node{ {Name: "httpEndpoint", Value: "foobar"}, {Name: "id128Bit", Value: "true"}, {Name: "sameSpan", Value: "true"}, {Name: "sampleRate", Value: "42"}, }}, }}, }, } assert.Equal(t, expected, node) } paerser-0.2.0/file/file_test.go000066400000000000000000000153101436254333400164050ustar00rootroot00000000000000package file import ( "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDecode_TOML(t *testing.T) { f, err := os.CreateTemp(t.TempDir(), "traefik-config-*.toml") require.NoError(t, err) t.Cleanup(func() { _ = f.Close() }) _, err = f.Write([]byte(` foo = "bar" fii = "bir" [yi] `)) require.NoError(t, err) element := &Yo{ Fuu: "test", } err = Decode(f.Name(), element) require.NoError(t, err) expected := &Yo{ Foo: "bar", Fii: "bir", Fuu: "test", Yi: &Yi{ Foo: "foo", Fii: "fii", }, } assert.Equal(t, expected, element) } func TestDecodeContent_TOML(t *testing.T) { content := ` foo = "bar" fii = "bir" [yi] ` element := &Yo{ Fuu: "test", } err := DecodeContent(content, ".toml", element) require.NoError(t, err) expected := &Yo{ Foo: "bar", Fii: "bir", Fuu: "test", Yi: &Yi{ Foo: "foo", Fii: "fii", }, } assert.Equal(t, expected, element) } func TestDecodeContent_TOML_rawSlice(t *testing.T) { //nolint:dupword // false positive content := ` [testData] trustIP = [ "10.0.0.0/8", "172.0.0.0/8", "192.0.0.0/8" ] koo = [1, 2, 3] soo = [1, "a", 3] boo = [1, 2.6, 3] hoo = [true, false, false, true] buckets = [42.01, 42.02] [testData.Headers] Foo = "Bar" ` var element FooRaw err := DecodeContent(content, ".toml", &element) require.NoError(t, err) expected := FooRaw{ TestData: map[string]interface{}{ "Headers": map[string]interface{}{"Foo": "Bar"}, "trustIP": []interface{}{"10.0.0.0/8", "172.0.0.0/8", "192.0.0.0/8"}, "koo": []interface{}{int64(1), int64(2), int64(3)}, "soo": []interface{}{"1", "a", "3"}, "boo": []interface{}{float64(1), 2.6, float64(3)}, "hoo": []interface{}{true, false, false, true}, "buckets": []interface{}{42.01, 42.02}, }, } assert.EqualValues(t, expected, element) } func TestDecodeContent_TOML_rawValue(t *testing.T) { content := ` name = "test" [[meta.aaa]] bbb = 1 ` type Foo struct { Name string Meta map[string]interface{} } element := &Foo{} err := DecodeContent(content, ".toml", element) require.NoError(t, err) expected := &Foo{ Name: "test", Meta: map[string]interface{}{"aaa": []interface{}{map[string]interface{}{"bbb": "1"}}}, } assert.Equal(t, expected, element) } func TestDecode_YAML(t *testing.T) { f, err := os.CreateTemp(t.TempDir(), "traefik-config-*.yaml") require.NoError(t, err) t.Cleanup(func() { _ = f.Close() }) _, err = f.Write([]byte(` foo: bar fii: bir yi: {} `)) require.NoError(t, err) element := &Yo{ Fuu: "test", } err = Decode(f.Name(), element) require.NoError(t, err) expected := &Yo{ Foo: "bar", Fii: "bir", Fuu: "test", Yi: &Yi{ Foo: "foo", Fii: "fii", }, } assert.Equal(t, expected, element) } func TestDecodeContent_YAML(t *testing.T) { content := ` foo: bar fii: bir yi: {} ` element := &Yo{ Fuu: "test", } err := DecodeContent(content, ".yaml", element) require.NoError(t, err) expected := &Yo{ Foo: "bar", Fii: "bir", Fuu: "test", Yi: &Yi{ Foo: "foo", Fii: "fii", }, } assert.Equal(t, expected, element) } func TestDecodeContent_YAML_rawSlice(t *testing.T) { content := ` testData: Headers: Foo: Bar trustIP: - 10.0.0.0/8 - 172.0.0.0/8 - 192.0.0.0/8 koo: - 1 - 2 - 3 soo: - 1 - a - 3 boo: - 1 - 2.6 - 3 buckets: - 42.01 - 42.02 hoo: - true - false - false - true joo: - name: a value: 1 - name: c value: 2 ` var element FooRaw err := DecodeContent(content, ".yaml", &element) require.NoError(t, err) expected := FooRaw{ TestData: map[string]interface{}{ "Headers": map[string]interface{}{"Foo": "Bar"}, "trustIP": []interface{}{"10.0.0.0/8", "172.0.0.0/8", "192.0.0.0/8"}, "koo": []interface{}{int64(1), int64(2), int64(3)}, "soo": []interface{}{"1", "a", "3"}, "boo": []interface{}{float64(1), 2.6, float64(3)}, "hoo": []interface{}{true, false, false, true}, "buckets": []interface{}{42.01, 42.02}, "joo": []interface{}{ map[string]interface{}{"name": "a", "value": "1"}, map[string]interface{}{"name": "c", "value": "2"}, }, }, } assert.Equal(t, expected, element) } func TestDecodeContent_YAML_rawValue(t *testing.T) { type Foo struct { Name string Meta map[string]interface{} } testCases := []struct { desc string content string expected interface{} }{ { desc: "simple", content: ` name: test meta: aaa: - bbb: 1 `, expected: &Foo{ Name: "test", Meta: map[string]interface{}{"aaa": []interface{}{map[string]interface{}{"bbb": "1"}}}, }, }, { desc: "null", content: ` name: test meta: aaa: - bbb: 1 bbb: {"foo":"bar"} null: {"toto":"tata"} `, expected: &Foo{ Name: "test", Meta: map[string]interface{}{ "aaa": []interface{}{map[string]interface{}{"bbb": "1"}}, "bbb": map[string]interface{}{"foo": "bar"}, }, }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() element := &Foo{} err := DecodeContent(test.content, ".yaml", element) require.NoError(t, err) assert.Equal(t, test.expected, element) }) } } func TestDecodeContent_YAML_typedSliceRecursive(t *testing.T) { content := ` foo: - foo - bar bar: foo: - foo - bar baz: foo: - foo - bar boz: foo: - 42 - 42 ` element := map[string]interface{}{} err := DecodeContent(content, ".yaml", &element) require.NoError(t, err) expected := map[string]interface{}{ "foo": []interface{}{"foo", "bar"}, "bar": map[string]interface{}{ "foo": []interface{}{"foo", "bar"}, "baz": map[string]interface{}{ "foo": []interface{}{"foo", "bar"}, "boz": map[string]interface{}{ "foo": []interface{}{int64(42), int64(42)}, }, }, }, } assert.Equal(t, expected, element) } func TestDecodeContent_JSON(t *testing.T) { content := ` { "foo": "bar", "fii": "bir", "yi": {} } ` element := &Yo{ Fuu: "test", } err := DecodeContent(content, ".json", element) require.NoError(t, err) expected := &Yo{ Foo: "bar", Fii: "bir", Fuu: "test", Yi: &Yi{ Foo: "foo", Fii: "fii", }, } assert.Equal(t, expected, element) } func TestDecodeContent_JSON_rawValue(t *testing.T) { content := ` { "name": "test", "meta": { "aaa": [ { "bbb": 1 } ] } } ` type Foo struct { Name string Meta map[string]interface{} } element := &Foo{} err := DecodeContent(content, ".json", element) require.NoError(t, err) expected := &Foo{ Name: "test", Meta: map[string]interface{}{"aaa": []interface{}{map[string]interface{}{"bbb": "1"}}}, } assert.Equal(t, expected, element) } paerser-0.2.0/file/fixtures/000077500000000000000000000000001436254333400157515ustar00rootroot00000000000000paerser-0.2.0/file/fixtures/empty.toml000066400000000000000000000000001436254333400177720ustar00rootroot00000000000000paerser-0.2.0/file/fixtures/no_conf.toml000066400000000000000000000000251436254333400202640ustar00rootroot00000000000000[foo] bar = "test" paerser-0.2.0/file/fixtures/sample.json000066400000000000000000000142041436254333400201260ustar00rootroot00000000000000{ "global": { "checkNewVersion": true, "sendAnonymousUsage": true }, "serversTransport": { "insecureSkipVerify": true, "rootCAs": ["foobar", "foobar"], "maxIdleConnsPerHost": 42, "forwardingTimeouts": { "dialTimeout": 42, "responseHeaderTimeout": 42, "idleConnTimeout": 42 } }, "entryPoints": { "EntryPoint0": { "address": "foobar", "transport": { "lifeCycle": { "requestAcceptGraceTimeout": 42, "graceTimeOut": 42 }, "respondingTimeouts": { "readTimeout": 42, "writeTimeout": 42, "idleTimeout": 42 } }, "proxyProtocol": { "insecure": true, "trustedIPs": ["foobar", "foobar"] }, "forwardedHeaders": { "insecure": true, "trustedIPs": ["foobar", "foobar"] } } }, "providers": { "providersThrottleDuration": 42, "docker": { "constraints": "foobar", "watch": true, "endpoint": "foobar", "defaultRule": "foobar", "tls": { "ca": "foobar", "caOptional": true, "cert": "foobar", "key": "foobar", "insecureSkipVerify": true }, "exposedByDefault": true, "useBindPortIP": true, "swarmMode": true, "network": "foobar", "swarmModeRefreshSeconds": 42 }, "file": { "directory": "foobar", "watch": true, "filename": "foobar", "debugLogGeneratedTemplate": true }, "marathon": { "constraints": "foobar", "trace": true, "watch": true, "endpoint": "foobar", "defaultRule": "foobar", "exposedByDefault": true, "dcosToken": "foobar", "tls": { "ca": "foobar", "caOptional": true, "cert": "foobar", "key": "foobar", "insecureSkipVerify": true }, "dialerTimeout": 42, "responseHeaderTimeout": 42, "tlsHandshakeTimeout": 42, "keepAlive": 42, "forceTaskHostname": true, "basic": { "httpBasicAuthUser": "foobar", "httpBasicPassword": "foobar" }, "respectReadinessChecks": true }, "kubernetesIngress": { "endpoint": "foobar", "token": "foobar", "certAuthFilePath": "foobar", "disablePassHostHeaders": true, "namespaces": ["foobar", "foobar"], "labelSelector": "foobar", "ingressClass": "foobar", "ingressEndpoint": { "ip": "foobar", "hostname": "foobar", "publishedService": "foobar" } }, "kubernetesCRD": { "endpoint": "foobar", "token": "foobar", "certAuthFilePath": "foobar", "disablePassHostHeaders": true, "namespaces": ["foobar", "foobar"], "labelSelector": "foobar", "ingressClass": "foobar" }, "rest": { "entryPoint": "foobar" }, "rancher": { "constraints": "foobar", "watch": true, "defaultRule": "foobar", "exposedByDefault": true, "enableServiceHealthFilter": true, "refreshSeconds": 42, "intervalPoll": true, "prefix": "foobar" } }, "api": { "entryPoint": "foobar", "dashboard": true, "statistics": { "recentErrors": 42 }, "middlewares": ["foobar", "foobar"] }, "metrics": { "prometheus": { "buckets": [42.01, 42.02], "entryPoint": "foobar", "middlewares": ["foobar", "foobar"] }, "datadog": { "address": "foobar", "pushInterval": "10s" }, "statsD": { "address": "foobar", "pushInterval": "10s" }, "influxDB": { "address": "foobar", "protocol": "foobar", "pushInterval": "10s", "database": "foobar", "retentionPolicy": "foobar", "username": "foobar", "password": "foobar" } }, "ping": { "entryPoint": "foobar", "middlewares": ["foobar", "foobar"] }, "log": { "level": "foobar", "filePath": "foobar", "format": "foobar" }, "accessLog": { "filePath": "foobar", "format": "foobar", "filters": { "statusCodes": ["foobar", "foobar"], "retryAttempts": true, "minDuration": 42 }, "fields": { "defaultMode": "foobar", "names": { "name0": "foobar", "name1": "foobar" }, "headers": { "defaultMode": "foobar", "names": { "name0": "foobar", "name1": "foobar" } } }, "bufferingSize": 42 }, "tracing": { "serviceName": "foobar", "spanNameLimit": 42, "jaeger": { "samplingServerURL": "foobar", "samplingType": "foobar", "samplingParam": 42, "localAgentHostPort": "foobar", "gen128Bit": true, "propagation": "foobar", "traceContextHeaderName": "foobar" }, "zipkin": { "httpEndpoint": "foobar", "sameSpan": true, "id128Bit": true, "sampleRate": 42 }, "datadog": { "localAgentHostPort": "foobar", "globalTag": "foobar", "debug": true, "prioritySampling": true, "traceIDHeaderName": "foobar", "parentIDHeaderName": "foobar", "samplingPriorityHeaderName": "foobar", "bagagePrefixHeaderName": "foobar" }, "instana": { "localAgentHost": "foobar", "localAgentPort": 42, "logLevel": "foobar" }, "haystack": { "localAgentHost": "foobar", "localAgentPort": 42, "globalTag": "foobar", "traceIDHeaderName": "foobar", "parentIDHeaderName": "foobar", "spanIDHeaderName": "foobar" } }, "hostResolver": { "cnameFlattening": true, "resolvConfig": "foobar", "resolvDepth": 42 }, "certificatesResolvers": { "default": { "acme": { "email": "foobar", "acmeLogging": true, "caServer": "foobar", "storage": "foobar", "entryPoint": "foobar", "keyType": "foobar", "dnsChallenge": { "provider": "foobar", "delayBeforeCheck": 42, "resolvers": ["foobar", "foobar"], "disablePropagationCheck": true }, "httpChallenge": { "entryPoint": "foobar" }, "tlsChallenge": {} } } } } paerser-0.2.0/file/fixtures/sample.toml000066400000000000000000000335501436254333400201350ustar00rootroot00000000000000[global] checkNewVersion = true sendAnonymousUsage = true [serversTransport] insecureSkipVerify = true rootCAs = ["foobar", "foobar"] maxIdleConnsPerHost = 42 [serversTransport.forwardingTimeouts] dialTimeout = 42 responseHeaderTimeout = 42 idleConnTimeout = 42 [entryPoints] [entryPoints.EntryPoint0] address = "foobar" [entryPoints.EntryPoint0.transport] [entryPoints.EntryPoint0.transport.lifeCycle] requestAcceptGraceTimeout = 42 graceTimeOut = 42 [entryPoints.EntryPoint0.transport.respondingTimeouts] readTimeout = 42 writeTimeout = 42 idleTimeout = 42 [entryPoints.EntryPoint0.proxyProtocol] insecure = true trustedIPs = ["foobar", "foobar"] [entryPoints.EntryPoint0.forwardedHeaders] insecure = true trustedIPs = ["foobar", "foobar"] [providers] providersThrottleDuration = 42 [providers.docker] constraints = "foobar" watch = true endpoint = "foobar" defaultRule = "foobar" exposedByDefault = true useBindPortIP = true swarmMode = true network = "foobar" swarmModeRefreshSeconds = 42 [providers.docker.tls] ca = "foobar" caOptional = true cert = "foobar" key = "foobar" insecureSkipVerify = true [providers.file] directory = "foobar" watch = true filename = "foobar" debugLogGeneratedTemplate = true [providers.marathon] constraints = "foobar" trace = true watch = true endpoint = "foobar" defaultRule = "foobar" exposedByDefault = true dcosToken = "foobar" dialerTimeout = 42 responseHeaderTimeout = 42 tlsHandshakeTimeout = 42 keepAlive = 42 forceTaskHostname = true respectReadinessChecks = true [providers.marathon.tls] ca = "foobar" caOptional = true cert = "foobar" key = "foobar" insecureSkipVerify = true [providers.marathon.basic] httpBasicAuthUser = "foobar" httpBasicPassword = "foobar" [providers.kubernetesIngress] endpoint = "foobar" token = "foobar" certAuthFilePath = "foobar" disablePassHostHeaders = true namespaces = ["foobar", "foobar"] labelSelector = "foobar" ingressClass = "foobar" [providers.kubernetesIngress.ingressEndpoint] ip = "foobar" hostname = "foobar" publishedService = "foobar" [providers.kubernetesCRD] endpoint = "foobar" token = "foobar" certAuthFilePath = "foobar" disablePassHostHeaders = true namespaces = ["foobar", "foobar"] labelSelector = "foobar" ingressClass = "foobar" [providers.rest] entryPoint = "foobar" [providers.rancher] constraints = "foobar" watch = true defaultRule = "foobar" exposedByDefault = true enableServiceHealthFilter = true refreshSeconds = 42 intervalPoll = true prefix = "foobar" [api] entryPoint = "foobar" dashboard = true middlewares = ["foobar", "foobar"] [api.statistics] recentErrors = 42 [metrics] [metrics.prometheus] buckets = [42.01, 42.02] entryPoint = "foobar" middlewares = ["foobar", "foobar"] [metrics.datadog] address = "foobar" pushInterval = "10s" [metrics.statsD] address = "foobar" pushInterval = "10s" [metrics.influxDB] address = "foobar" protocol = "foobar" pushInterval = "10s" database = "foobar" retentionPolicy = "foobar" username = "foobar" password = "foobar" [ping] entryPoint = "foobar" middlewares = ["foobar", "foobar"] [log] level = "foobar" filePath = "foobar" format = "foobar" [accessLog] filePath = "foobar" format = "foobar" bufferingSize = 42 [accessLog.filters] statusCodes = ["foobar", "foobar"] retryAttempts = true minDuration = 42 [accessLog.fields] defaultMode = "foobar" [accessLog.fields.names] name0 = "foobar" name1 = "foobar" [accessLog.fields.headers] defaultMode = "foobar" [accessLog.fields.headers.names] name0 = "foobar" name1 = "foobar" [tracing] serviceName = "foobar" spanNameLimit = 42 [tracing.jaeger] samplingServerURL = "foobar" samplingType = "foobar" samplingParam = 42.0 localAgentHostPort = "foobar" gen128Bit = true propagation = "foobar" traceContextHeaderName = "foobar" [tracing.zipkin] httpEndpoint = "foobar" sameSpan = true id128Bit = true sampleRate = 42.0 [tracing.datadog] localAgentHostPort = "foobar" globalTag = "foobar" debug = true prioritySampling = true traceIDHeaderName = "foobar" parentIDHeaderName = "foobar" samplingPriorityHeaderName = "foobar" bagagePrefixHeaderName = "foobar" [tracing.instana] localAgentHost = "foobar" localAgentPort = 42 logLevel = "foobar" [tracing.haystack] localAgentHost = "foobar" localAgentPort = 42 globalTag = "foobar" traceIDHeaderName = "foobar" parentIDHeaderName = "foobar" spanIDHeaderName = "foobar" [hostResolver] cnameFlattening = true resolvConfig = "foobar" resolvDepth = 42 [certificatesResolvers.default.acme] email = "foobar" acmeLogging = true caServer = "foobar" storage = "foobar" entryPoint = "foobar" keyType = "foobar" [certificatesResolvers.default.acme.dnsChallenge] provider = "foobar" delayBeforeCheck = 42 resolvers = ["foobar", "foobar"] disablePropagationCheck = true [certificatesResolvers.default.acme.httpChallenge] entryPoint = "foobar" [certificatesResolvers.default.acme.tlsChallenge] ## Dynamic configuration [http] [http.routers] [http.routers.Router0] entryPoints = ["foobar", "foobar"] middlewares = ["foobar", "foobar"] service = "foobar" rule = "foobar" priority = 42 [http.routers.Router0.tls] [http.middlewares] [http.middlewares.Middleware0] [http.middlewares.Middleware0.addPrefix] prefix = "foobar" [http.middlewares.Middleware1] [http.middlewares.Middleware1.stripPrefix] prefixes = ["foobar", "foobar"] [http.middlewares.Middleware10] [http.middlewares.Middleware10.rateLimit] average = 42 burst = 42 [http.middlewares.Middleware10.rateLimit.sourceCriterion] requestHeaderName = "foobar" requestHost = true [http.middlewares.Middleware10.rateLimit.sourceCriterion.ipStrategy] depth = 42 excludedIPs = ["foobar", "foobar"] [http.middlewares.Middleware11] [http.middlewares.Middleware11.redirectRegex] regex = "foobar" replacement = "foobar" permanent = true [http.middlewares.Middleware12] [http.middlewares.Middleware12.redirectScheme] scheme = "foobar" port = "foobar" permanent = true [http.middlewares.Middleware13] [http.middlewares.Middleware13.basicAuth] users = ["foobar", "foobar"] usersFile = "foobar" realm = "foobar" removeHeader = true headerField = "foobar" [http.middlewares.Middleware14] [http.middlewares.Middleware14.digestAuth] users = ["foobar", "foobar"] usersFile = "foobar" removeHeader = true realm = "foobar" headerField = "foobar" [http.middlewares.Middleware15] [http.middlewares.Middleware15.forwardAuth] address = "foobar" trustForwardHeader = true authResponseHeaders = ["foobar", "foobar"] [http.middlewares.Middleware15.forwardAuth.tls] ca = "foobar" caOptional = true cert = "foobar" key = "foobar" insecureSkipVerify = true [http.middlewares.Middleware16] [http.middlewares.Middleware16.inFlightReq] amount = 42 [http.middlewares.Middleware16.inFlightReq.sourceCriterion] requestHeaderName = "foobar" requestHost = true [http.middlewares.Middleware16.inFlightReq.sourceCriterion.ipStrategy] depth = 42 excludedIPs = ["foobar", "foobar"] [http.middlewares.Middleware17] [http.middlewares.Middleware17.buffering] maxRequestBodyBytes = 42 memRequestBodyBytes = 42 maxResponseBodyBytes = 42 memResponseBodyBytes = 42 retryExpression = "foobar" [http.middlewares.Middleware18] [http.middlewares.Middleware18.circuitBreaker] expression = "foobar" [http.middlewares.Middleware19] [http.middlewares.Middleware19.compress] [http.middlewares.Middleware2] [http.middlewares.Middleware2.stripPrefixRegex] regex = ["foobar", "foobar"] [http.middlewares.Middleware20] [http.middlewares.Middleware20.passTLSClientCert] pem = true [http.middlewares.Middleware20.passTLSClientCert.info] notAfter = true notBefore = true sans = true [http.middlewares.Middleware20.passTLSClientCert.info.subject] country = true province = true locality = true organization = true commonName = true serialNumber = true domainComponent = true [http.middlewares.Middleware20.passTLSClientCert.info.issuer] country = true province = true locality = true organization = true commonName = true serialNumber = true domainComponent = true [http.middlewares.Middleware21] [http.middlewares.Middleware21.retry] regex = 0 [http.middlewares.Middleware3] [http.middlewares.Middleware3.replacePath] path = "foobar" [http.middlewares.Middleware4] [http.middlewares.Middleware4.replacePathRegex] regex = "foobar" replacement = "foobar" [http.middlewares.Middleware5] [http.middlewares.Middleware5.chain] middlewares = ["foobar", "foobar"] [http.middlewares.Middleware6] [http.middlewares.Middleware6.ipWhiteList] sourceRange = ["foobar", "foobar"] [http.middlewares.Middleware7] [http.middlewares.Middleware7.ipWhiteList] [http.middlewares.Middleware7.ipWhiteList.ipStrategy] depth = 42 excludedIPs = ["foobar", "foobar"] [http.middlewares.Middleware8] [http.middlewares.Middleware8.headers] accessControlAllowCredentials = true accessControlAllowHeaders = ["foobar", "foobar"] accessControlAllowMethods = ["foobar", "foobar"] accessControlAllowOrigin = "foobar" accessControlExposeHeaders = ["foobar", "foobar"] accessControlMaxAge = 42 addVaryHeader = true allowedHosts = ["foobar", "foobar"] hostsProxyHeaders = ["foobar", "foobar"] sslRedirect = true sslTemporaryRedirect = true sslHost = "foobar" sslForceHost = true stsSeconds = 42 stsIncludeSubdomains = true stsPreload = true forceSTSHeader = true frameDeny = true customFrameOptionsValue = "foobar" contentTypeNosniff = true browserXssFilter = true customBrowserXSSValue = "foobar" contentSecurityPolicy = "foobar" publicKey = "foobar" referrerPolicy = "foobar" featurePolicy = "foobar" isDevelopment = true [http.middlewares.Middleware8.headers.customRequestHeaders] name0 = "foobar" name1 = "foobar" [http.middlewares.Middleware8.headers.customResponseHeaders] name0 = "foobar" name1 = "foobar" [http.middlewares.Middleware8.headers.sslProxyHeaders] name0 = "foobar" name1 = "foobar" [http.middlewares.Middleware9] [http.middlewares.Middleware9.errors] status = ["foobar", "foobar"] service = "foobar" query = "foobar" [http.services] [http.services.Service0] [http.services.Service0.loadBalancer] passHostHeader = true [http.services.Service0.loadBalancer.sticky.cookie] name = "foobar" [[http.services.Service0.loadBalancer.servers]] url = "foobar" [[http.services.Service0.loadBalancer.servers]] url = "foobar" [http.services.Service0.loadBalancer.healthCheck] scheme = "foobar" path = "foobar" port = 42 interval = "foobar" timeout = "foobar" hostname = "foobar" [http.services.Service0.loadBalancer.healthCheck.headers] name0 = "foobar" name1 = "foobar" [http.services.Service0.loadBalancer.responseForwarding] flushInterval = "foobar" [tcp] [tcp.routers] [tcp.routers.TCPRouter0] entryPoints = ["foobar", "foobar"] service = "foobar" rule = "foobar" [tcp.routers.TCPRouter0.tls] passthrough = true [tcp.services] [tcp.services.TCPService0] [tcp.services.TCPService0.loadBalancer] [[tcp.services.TCPService0.loadBalancer.servers]] address = "foobar" [[tcp.services.TCPService0.loadBalancer.servers]] address = "foobar" [tls] [[tls.Certificates]] certFile = "foobar" keyFile = "foobar" stores = ["foobar", "foobar"] [[tls.Certificates]] certFile = "foobar" keyFile = "foobar" stores = ["foobar", "foobar"] [tls.options] [tls.options.TLS0] minVersion = "foobar" cipherSuites = ["foobar", "foobar"] sniStrict = true [tls.options.TLS0.clientAuth] caFiles = ["foobar", "foobar"] clientAuthType = "VerifyClientCertIfGiven" [tls.options.TLS1] minVersion = "foobar" cipherSuites = ["foobar", "foobar"] sniStrict = true [tls.options.TLS1.clientAuth] caFiles = ["foobar", "foobar"] clientAuthType = "VerifyClientCertIfGiven" [tls.stores] [tls.stores.Store0] [tls.stores.Store0.defaultCertificate] certFile = "foobar" keyFile = "foobar" [tls.stores.Store1] [tls.stores.Store1.defaultCertificate] certFile = "foobar" keyFile = "foobar" paerser-0.2.0/file/fixtures/sample.yml000066400000000000000000000112601436254333400177550ustar00rootroot00000000000000global: checkNewVersion: true sendAnonymousUsage: true serversTransport: insecureSkipVerify: true rootCAs: - foobar - foobar maxIdleConnsPerHost: 42 forwardingTimeouts: dialTimeout: 42 responseHeaderTimeout: 42 idleConnTimeout: 42 entryPoints: EntryPoint0: address: foobar transport: lifeCycle: requestAcceptGraceTimeout: 42 graceTimeOut: 42 respondingTimeouts: readTimeout: 42 writeTimeout: 42 idleTimeout: 42 proxyProtocol: insecure: true trustedIPs: - foobar - foobar forwardedHeaders: insecure: true trustedIPs: - foobar - foobar providers: providersThrottleDuration: 42 docker: constraints: foobar watch: true endpoint: foobar defaultRule: foobar tls: ca: foobar caOptional: true cert: foobar key: foobar insecureSkipVerify: true exposedByDefault: true useBindPortIP: true swarmMode: true network: foobar swarmModeRefreshSeconds: 42 file: directory: foobar watch: true filename: foobar debugLogGeneratedTemplate: true marathon: constraints: foobar trace: true watch: true endpoint: foobar defaultRule: foobar exposedByDefault: true dcosToken: foobar tls: ca: foobar caOptional: true cert: foobar key: foobar insecureSkipVerify: true dialerTimeout: 42 responseHeaderTimeout: 42 tlsHandshakeTimeout: 42 keepAlive: 42 forceTaskHostname: true basic: httpBasicAuthUser: foobar httpBasicPassword: foobar respectReadinessChecks: true kubernetesIngress: endpoint: foobar token: foobar certAuthFilePath: foobar disablePassHostHeaders: true namespaces: - foobar - foobar labelSelector: foobar ingressClass: foobar ingressEndpoint: ip: foobar hostname: foobar publishedService: foobar kubernetesCRD: endpoint: foobar token: foobar certAuthFilePath: foobar disablePassHostHeaders: true namespaces: - foobar - foobar labelSelector: foobar ingressClass: foobar rest: entryPoint: foobar rancher: constraints: foobar watch: true defaultRule: foobar exposedByDefault: true enableServiceHealthFilter: true refreshSeconds: 42 intervalPoll: true prefix: foobar api: entryPoint: foobar dashboard: true statistics: recentErrors: 42 middlewares: - foobar - foobar metrics: prometheus: buckets: - 42.01 - 42.02 entryPoint: foobar middlewares: - foobar - foobar datadog: address: foobar pushInterval: 10s statsD: address: foobar pushInterval: 10s influxDB: address: foobar protocol: foobar pushInterval: 10s database: foobar retentionPolicy: foobar username: foobar password: foobar ping: entryPoint: foobar middlewares: - foobar - foobar log: level: foobar filePath: foobar format: foobar accessLog: filePath: foobar format: foobar filters: statusCodes: - foobar - foobar retryAttempts: true minDuration: 42 fields: defaultMode: foobar names: name0: foobar name1: foobar headers: defaultMode: foobar names: name0: foobar name1: foobar bufferingSize: 42 tracing: serviceName: foobar spanNameLimit: 42 jaeger: samplingServerURL: foobar samplingType: foobar samplingParam: 42 localAgentHostPort: foobar gen128Bit: true propagation: foobar traceContextHeaderName: foobar zipkin: httpEndpoint: foobar sameSpan: true id128Bit: true sampleRate: 42 datadog: localAgentHostPort: foobar globalTag: foobar debug: true prioritySampling: true traceIDHeaderName: foobar parentIDHeaderName: foobar samplingPriorityHeaderName: foobar bagagePrefixHeaderName: foobar instana: localAgentHost: foobar localAgentPort: 42 logLevel: foobar haystack: localAgentHost: foobar localAgentPort: 42 globalTag: foobar traceIDHeaderName: foobar parentIDHeaderName: foobar spanIDHeaderName: foobar hostResolver: cnameFlattening: true resolvConfig: foobar resolvDepth: 42 certificatesResolvers: default: acme: email: foobar acmeLogging: true caServer: foobar storage: foobar entryPoint: foobar keyType: foobar dnsChallenge: provider: foobar delayBeforeCheck: 42 resolvers: - foobar - foobar disablePropagationCheck: true httpChallenge: entryPoint: foobar tlsChallenge: {} paerser-0.2.0/file/fixtures_test.go000066400000000000000000000006211436254333400173360ustar00rootroot00000000000000package file type bar string type Yo struct { Foo string Fii string Fuu string Yi *Yi `file:"allowEmpty"` } func (y *Yo) SetDefaults() { y.Foo = "foo" y.Fii = "fii" } type Yi struct { Foo string Fii string Fuu string } func (y *Yi) SetDefaults() { y.Foo = "foo" y.Fii = "fii" } type Yu struct { Yi } type Ye struct { *Yi } type FooRaw struct { TestData map[string]interface{} } paerser-0.2.0/file/raw_node.go000066400000000000000000000105001436254333400162210ustar00rootroot00000000000000package file import ( "fmt" "reflect" "sort" "strconv" "strings" "github.com/traefik/paerser/parser" ) func decodeRawToNode(data map[string]interface{}, filters ...string) (*parser.Node, error) { root := &parser.Node{ Name: parser.DefaultRootName, } vData := reflect.ValueOf(data) err := decodeRaw(root, vData, filters...) if err != nil { return nil, err } return root, nil } func decodeRaw(node *parser.Node, vData reflect.Value, filters ...string) error { sortedKeys := sortKeys(vData, filters) for _, key := range sortedKeys { if key.Kind() == reflect.Invalid { continue } if vData.MapIndex(key).IsNil() { continue } value := reflect.ValueOf(vData.MapIndex(key).Interface()) child := &parser.Node{Name: key.String()} switch value.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: fallthrough case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: fallthrough case reflect.Float32, reflect.Float64: fallthrough case reflect.Bool: fallthrough case reflect.String: value, err := getSimpleValue(value) if err != nil { return err } child.Value = value case reflect.Slice: var values []string var kind reflect.Kind for i := 0; i < value.Len(); i++ { item := value.Index(i) // Try to guess the kind of the slice. // TODO(ldez): it's related to raw map. Rethink the node parser. switch item.Kind() { case reflect.Interface: if kind < item.Elem().Kind() { kind = item.Elem().Kind() } case reflect.Map: // noop default: if kind < item.Kind() { kind = item.Kind() } } switch item.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: fallthrough case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: fallthrough case reflect.Bool: fallthrough case reflect.String: fallthrough case reflect.Map: fallthrough case reflect.Interface: sValue := reflect.ValueOf(item.Interface()) if sValue.Kind() == reflect.Map { ch := &parser.Node{ Name: "[" + strconv.Itoa(i) + "]", } child.Children = append(child.Children, ch) err := decodeRaw(ch, sValue) if err != nil { return err } } else { val, err := getSimpleValue(sValue) if err != nil { return err } values = append(values, val) } default: return fmt.Errorf("field %s uses unsupported slice type: %s", child.Name, item.Kind().String()) } } // TODO(ldez): the kind is related to raw map. Rethink the node parser. child.Value = "" if len(values) > 0 { child.Value = fmt.Sprintf("%[1]s%[2]d%[1]s%[3]s", defaultRawSliceSeparator, kind, strings.Join(values, defaultRawSliceSeparator)) } case reflect.Map: err := decodeRaw(child, value) if err != nil { return err } default: return fmt.Errorf("field %s uses unsupported type: %s", child.Name, value.Kind().String()) } node.Children = append(node.Children, child) } return nil } func getSimpleValue(item reflect.Value) (string, error) { switch item.Kind() { case reflect.String: return item.String(), nil case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return strconv.FormatInt(item.Int(), 10), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return strconv.FormatUint(item.Uint(), 10), nil case reflect.Float32, reflect.Float64: return strings.TrimSuffix(strconv.FormatFloat(item.Float(), 'f', 6, 64), ".000000"), nil case reflect.Bool: return strconv.FormatBool(item.Bool()), nil default: return "", fmt.Errorf("unsupported simple value type: %s", item.Kind().String()) } } func sortKeys(vData reflect.Value, filters []string) []reflect.Value { var sortedKeys []reflect.Value for _, v := range vData.MapKeys() { rValue := reflect.ValueOf(v.Interface()) key := rValue.String() if len(filters) == 0 { sortedKeys = append(sortedKeys, rValue) continue } for _, filter := range filters { if strings.EqualFold(key, filter) { sortedKeys = append(sortedKeys, rValue) continue } } } sort.Slice(sortedKeys, func(i, j int) bool { return sortedKeys[i].String() < sortedKeys[j].String() }) return sortedKeys } paerser-0.2.0/file/raw_node_test.go000066400000000000000000000262601436254333400172720ustar00rootroot00000000000000package file import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/traefik/paerser/parser" ) func Test_decodeRawToNode(t *testing.T) { testCases := []struct { desc string data map[string]interface{} expected *parser.Node }{ { desc: "empty", data: map[string]interface{}{}, expected: &parser.Node{ Name: "traefik", }, }, { desc: "string", data: map[string]interface{}{ "foo": "bar", }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "bar"}, }, }, }, { desc: "string named type", data: map[string]interface{}{ "foo": bar("bar"), }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "bar"}, }, }, }, { desc: "bool", data: map[string]interface{}{ "foo": true, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "true"}, }, }, }, { desc: "int", data: map[string]interface{}{ "foo": 1, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "1"}, }, }, }, { desc: "int8", data: map[string]interface{}{ "foo": int8(1), }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "1"}, }, }, }, { desc: "int16", data: map[string]interface{}{ "foo": int16(1), }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "1"}, }, }, }, { desc: "int32", data: map[string]interface{}{ "foo": int32(1), }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "1"}, }, }, }, { desc: "int64", data: map[string]interface{}{ "foo": int64(1), }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "1"}, }, }, }, { desc: "uint", data: map[string]interface{}{ "foo": uint(1), }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "1"}, }, }, }, { desc: "uint8", data: map[string]interface{}{ "foo": uint8(1), }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "1"}, }, }, }, { desc: "uint16", data: map[string]interface{}{ "foo": uint16(1), }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "1"}, }, }, }, { desc: "uint32", data: map[string]interface{}{ "foo": uint32(1), }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "1"}, }, }, }, { desc: "uint64", data: map[string]interface{}{ "foo": uint64(1), }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "1"}, }, }, }, { desc: "float32", data: map[string]interface{}{ "foo": float32(1), }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "1"}, }, }, }, { desc: "float64", data: map[string]interface{}{ "foo": float64(1), }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "1"}, }, }, }, { desc: "string slice", data: map[string]interface{}{ "foo": []string{"A", "B"}, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "â•‘24â•‘Aâ•‘B"}, }, }, }, { desc: "int slice", data: map[string]interface{}{ "foo": []int{1, 2}, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "â•‘2â•‘1â•‘2"}, }, }, }, { desc: "int8 slice", data: map[string]interface{}{ "foo": []int8{1, 2}, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "â•‘3â•‘1â•‘2"}, }, }, }, { desc: "int16 slice", data: map[string]interface{}{ "foo": []int16{1, 2}, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "â•‘4â•‘1â•‘2"}, }, }, }, { desc: "int32 slice", data: map[string]interface{}{ "foo": []int32{1, 2}, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "â•‘5â•‘1â•‘2"}, }, }, }, { desc: "int64 slice", data: map[string]interface{}{ "foo": []int64{1, 2}, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "â•‘6â•‘1â•‘2"}, }, }, }, { desc: "bool slice", data: map[string]interface{}{ "foo": []bool{true, false}, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "â•‘1â•‘trueâ•‘false"}, }, }, }, { desc: "interface (string) slice", data: map[string]interface{}{ "foo": []interface{}{"A", "B"}, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "â•‘24â•‘Aâ•‘B"}, }, }, }, { desc: "interface (int) slice", data: map[string]interface{}{ "foo": []interface{}{1, 2}, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Value: "â•‘2â•‘1â•‘2"}, }, }, }, { desc: "2 strings", data: map[string]interface{}{ "foo": "bar", "fii": "bir", }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "fii", Value: "bir"}, {Name: "foo", Value: "bar"}, }, }, }, { desc: "string, level 2", data: map[string]interface{}{ "fii": map[interface{}]interface{}{ "fuu": "bur", }, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "bur"}}}, }, }, }, { desc: "int, level 2", data: map[string]interface{}{ "fii": map[interface{}]interface{}{ "fuu": 1, }, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "1"}}}, }, }, }, { desc: "uint, level 2", data: map[string]interface{}{ "fii": map[interface{}]interface{}{ "fuu": uint(1), }, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "1"}}}, }, }, }, { desc: "bool, level 2", data: map[string]interface{}{ "fii": map[interface{}]interface{}{ "fuu": true, }, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "true"}}}, }, }, }, { desc: "string, level 3", data: map[string]interface{}{ "foo": map[interface{}]interface{}{ "fii": map[interface{}]interface{}{ "fuu": "bur", }, }, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Children: []*parser.Node{ {Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "bur"}}}, }}, }, }, }, { desc: "int, level 3", data: map[string]interface{}{ "fii": map[interface{}]interface{}{ "fuu": 1, }, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "1"}}}, }, }, }, { desc: "uint, level 3", data: map[string]interface{}{ "fii": map[interface{}]interface{}{ "fuu": uint(1), }, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "1"}}}, }, }, }, { desc: "bool, level 3", data: map[string]interface{}{ "fii": map[interface{}]interface{}{ "fuu": true, }, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "true"}}}, }, }, }, { desc: "struct", data: map[string]interface{}{ "foo": map[interface{}]interface{}{ "field1": "C", "field2": "C", }, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Children: []*parser.Node{ {Name: "field1", Value: "C"}, {Name: "field2", Value: "C"}, }}, }, }, }, { desc: "slice struct 1", data: map[string]interface{}{ "foo": []map[string]interface{}{ {"field1": "A", "field2": "A"}, {"field1": "B", "field2": "B"}, {"field2": "C", "field1": "C"}, }, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Children: []*parser.Node{ {Name: "[0]", Children: []*parser.Node{ {Name: "field1", Value: "A"}, {Name: "field2", Value: "A"}, }}, {Name: "[1]", Children: []*parser.Node{ {Name: "field1", Value: "B"}, {Name: "field2", Value: "B"}, }}, {Name: "[2]", Children: []*parser.Node{ {Name: "field1", Value: "C"}, {Name: "field2", Value: "C"}, }}, }}, }, }, }, { desc: "slice struct 2", data: map[string]interface{}{ "foo": []interface{}{ map[interface{}]interface{}{ "field2": "A", "field1": "A", }, map[interface{}]interface{}{ "field1": "B", "field2": "B", }, map[interface{}]interface{}{ "field1": "C", "field2": "C", }, }, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "foo", Children: []*parser.Node{ {Name: "[0]", Children: []*parser.Node{ {Name: "field1", Value: "A"}, {Name: "field2", Value: "A"}, }}, {Name: "[1]", Children: []*parser.Node{ {Name: "field1", Value: "B"}, {Name: "field2", Value: "B"}, }}, {Name: "[2]", Children: []*parser.Node{ {Name: "field1", Value: "C"}, {Name: "field2", Value: "C"}, }}, }}, }, }, }, { desc: "nil value", data: map[string]interface{}{ "fii": map[interface{}]interface{}{ "fuu": nil, }, }, expected: &parser.Node{ Name: "traefik", Children: []*parser.Node{ {Name: "fii"}, }, }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() node, err := decodeRawToNode(test.data) require.NoError(t, err) assert.Equal(t, test.expected, node) }) } } func Test_decodeRawToNode_errors(t *testing.T) { testCases := []struct { desc string data map[string]interface{} }{ { desc: "invalid type", data: map[string]interface{}{ "foo": struct{}{}, }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() _, err := decodeRawToNode(test.data) require.Error(t, err) }) } } paerser-0.2.0/flag/000077500000000000000000000000001436254333400140725ustar00rootroot00000000000000paerser-0.2.0/flag/flag.go000066400000000000000000000032601436254333400153330ustar00rootroot00000000000000// Package flag implements encoding and decoding between flag arguments and a typed Configuration. package flag import ( "github.com/traefik/paerser/parser" ) // Decode decodes the given flag arguments into the given element. // The operation goes through four stages roughly summarized as: // - flag arguments -> parsed map of flags // - map -> tree of untyped nodes // - untyped nodes -> nodes augmented with metadata such as kind (inferred from element) // - "typed" nodes -> typed element. func Decode(args []string, element interface{}) error { ref, err := Parse(args, element) if err != nil { return err } return parser.Decode(ref, element, parser.DefaultRootName) } // Encode encodes the configuration in element into the flags represented in the returned Flats. // The operation goes through three stages roughly summarized as: // - typed configuration in element -> tree of untyped nodes // - untyped nodes -> nodes augmented with metadata such as kind (inferred from element) // - "typed" nodes -> flags with default values (determined by type/kind). func Encode(element interface{}) ([]parser.Flat, error) { if element == nil { return nil, nil } etnOpts := parser.EncoderToNodeOpts{OmitEmpty: false, TagName: parser.TagLabel, AllowSliceAsStruct: true} node, err := parser.EncodeToNode(element, parser.DefaultRootName, etnOpts) if err != nil { return nil, err } metaOpts := parser.MetadataOpts{TagName: parser.TagLabel, AllowSliceAsStruct: true} err = parser.AddMetadata(element, node, metaOpts) if err != nil { return nil, err } flatOpts := parser.FlatOpts{Separator: ".", SkipRoot: true, TagName: parser.TagLabel} return parser.EncodeToFlat(element, node, flatOpts) } paerser-0.2.0/flag/flag_test.go000066400000000000000000000455011436254333400163760ustar00rootroot00000000000000package flag import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/traefik/paerser/generator" "github.com/traefik/paerser/parser" "github.com/traefik/paerser/types" ) func TestDecode(t *testing.T) { testCases := []struct { desc string args []string element interface{} expected interface{} }{ { desc: "no args", args: nil, expected: nil, }, { desc: "types.Duration value", args: []string{"--foo=1"}, element: &struct { Foo types.Duration }{}, expected: &struct { Foo types.Duration }{ Foo: types.Duration(1 * time.Second), }, }, { desc: "time.Duration value", args: []string{"--foo=1"}, element: &struct { Foo time.Duration }{}, expected: &struct { Foo time.Duration }{ Foo: 1 * time.Nanosecond, }, }, { desc: "bool value", args: []string{"--foo"}, element: &struct { Foo bool }{}, expected: &struct { Foo bool }{ Foo: true, }, }, { desc: "equal", args: []string{"--foo=bar"}, element: &struct { Foo string }{}, expected: &struct { Foo string }{ Foo: "bar", }, }, { desc: "space separated", args: []string{"--foo", "bar"}, element: &struct { Foo string }{}, expected: &struct { Foo string }{ Foo: "bar", }, }, { desc: "space separated with end of parameter", args: []string{"--foo=bir", "--", "--bar"}, element: &struct { Foo string }{}, expected: &struct { Foo string }{ Foo: "bir", }, }, { desc: "multiple bool flags without value", args: []string{"--foo", "--bar"}, element: &struct { Foo bool Bar bool }{}, expected: &struct { Foo bool Bar bool }{ Foo: true, Bar: true, }, }, { desc: "slice with several flags", args: []string{"--foo=bar", "--foo=baz"}, element: &struct { Foo []string }{}, expected: &struct { Foo []string }{ Foo: []string{"bar", "baz"}, }, }, { desc: "map string", args: []string{"--foo.name=bar"}, element: &struct { Foo map[string]string }{}, expected: &struct { Foo map[string]string }{ Foo: map[string]string{ "name": "bar", }, }, }, { desc: "map string case sensitive", args: []string{"--foo.caseSensitiveName=barBoo"}, element: &struct { Foo map[string]string }{}, expected: &struct { Foo map[string]string }{ Foo: map[string]string{ "caseSensitiveName": "barBoo", }, }, }, { desc: "map struct", args: []string{"--foo.name.value=bar"}, element: &struct { Foo map[string]struct{ Value string } }{}, expected: &struct { Foo map[string]struct{ Value string } }{ Foo: map[string]struct{ Value string }{ "name": { Value: "bar", }, }, }, }, { desc: "map struct with sub-struct", args: []string{"--foo.name.bar.value=bar"}, element: &struct { Foo map[string]struct { Bar *struct{ Value string } } }{}, expected: &struct { Foo map[string]struct { Bar *struct{ Value string } } }{ Foo: map[string]struct { Bar *struct{ Value string } }{ "name": { Bar: &struct { Value string }{ Value: "bar", }, }, }, }, }, { desc: "map struct with sub-map", args: []string{"--foo.name1.bar.name2.value=bar"}, element: &struct { Foo map[string]struct { Bar map[string]struct{ Value string } } }{}, expected: &struct { Foo map[string]struct { Bar map[string]struct{ Value string } } }{ Foo: map[string]struct { Bar map[string]struct{ Value string } }{ "name1": { Bar: map[string]struct{ Value string }{ "name2": { Value: "bar", }, }, }, }, }, }, { desc: "slice with several flags 2", args: []string{"--foo", "bar", "--foo", "baz"}, element: &struct { Foo []string }{}, expected: &struct { Foo []string }{ Foo: []string{"bar", "baz"}, }, }, { desc: "slice with several flags 3", args: []string{"--foo", "bar", "--foo=", "--baz"}, element: &struct { Foo []string Baz bool }{}, expected: &struct { Foo []string Baz bool }{ Foo: []string{"bar", ""}, Baz: true, }, }, { desc: "slice with several flags 4", args: []string{"--foo", "bar", "--foo", "--baz"}, element: &struct { Foo []string Baz bool }{}, expected: &struct { Foo []string Baz bool }{ Foo: []string{"bar", "--baz"}, }, }, { desc: "slice of struct", args: []string{ "--foo[0].Field1", "bar", "--foo[0].Field2", "6", "--foo[1].Field1", "bur", "--foo[1].Field2", "2", }, element: &struct { Foo []struct { Field1 string Field2 int } }{}, expected: &struct { Foo []struct { Field1 string Field2 int } }{ Foo: []struct { Field1 string Field2 int }{ { Field1: "bar", Field2: 6, }, { Field1: "bur", Field2: 2, }, }, }, }, { desc: "slice of pointer of struct", args: []string{ "--foo[0].Field1", "bar", "--foo[0].Field2", "6", "--foo[1].Field1", "bur", "--foo[1].Field2", "2", }, element: &struct { Foo []*struct { Field1 string Field2 int } }{}, expected: &struct { Foo []*struct { Field1 string Field2 int } }{ Foo: []*struct { Field1 string Field2 int }{ { Field1: "bar", Field2: 6, }, { Field1: "bur", Field2: 2, }, }, }, }, { desc: "multiple string flag", element: &struct { Foo string }{}, args: []string{"--foo=bar", "--foo=baz"}, expected: &struct { Foo string }{ Foo: "baz", }, }, { desc: "multiple string flag 2", element: &struct { Foo string }{}, args: []string{"--foo", "bar", "--foo", "baz"}, expected: &struct { Foo string }{ Foo: "baz", }, }, { desc: "string without value", element: &struct { Foo string Bar bool }{}, args: []string{"--foo", "--bar"}, expected: &struct { Foo string Bar bool }{ Foo: "--bar", }, }, { desc: "struct pointer value", args: []string{"--foo"}, element: &struct { Foo *struct{ Field string } `label:"allowEmpty"` }{}, expected: &struct { Foo *struct{ Field string } `label:"allowEmpty"` }{ Foo: &struct{ Field string }{}, }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() err := Decode(test.args, test.element) require.NoError(t, err) assert.Equal(t, test.expected, test.element) }) } } func TestEncode(t *testing.T) { testCases := []struct { desc string element interface{} expected []parser.Flat }{ { desc: "string field", element: &struct { Field string `description:"field description"` }{ Field: "test", }, expected: []parser.Flat{{ Name: "field", Description: "field description", Default: "test", }}, }, { desc: "int field", element: &struct { Field int `description:"field description"` }{ Field: 6, }, expected: []parser.Flat{{ Name: "field", Description: "field description", Default: "6", }}, }, { desc: "bool field", element: &struct { Field bool `description:"field description"` }{ Field: true, }, expected: []parser.Flat{{ Name: "field", Description: "field description", Default: "true", }}, }, { desc: "string pointer field", element: &struct { Field *string `description:"field description"` }{ Field: func(v string) *string { return &v }("test"), }, expected: []parser.Flat{{ Name: "field", Description: "field description", Default: "test", }}, }, { desc: "int pointer field", element: &struct { Field *int `description:"field description"` }{ Field: func(v int) *int { return &v }(6), }, expected: []parser.Flat{{ Name: "field", Description: "field description", Default: "6", }}, }, { desc: "bool pointer field", element: &struct { Field *bool `description:"field description"` }{ Field: func(v bool) *bool { return &v }(true), }, expected: []parser.Flat{{ Name: "field", Description: "field description", Default: "true", }}, }, { desc: "slice of string field, no initial value", element: &struct { Field []string `description:"field description"` }{}, expected: []parser.Flat{{ Name: "field", Description: "field description", Default: "", }}, }, { desc: "slice of string field, with initial value", element: &struct { Field []string `description:"field description"` }{ Field: []string{"foo", "bar"}, }, expected: []parser.Flat{{ Name: "field", Description: "field description", Default: "foo, bar", }}, }, { desc: "slice of int field, no initial value", element: &struct { Field []int `description:"field description"` }{}, expected: []parser.Flat{{ Name: "field", Description: "field description", Default: "", }}, }, { desc: "slice of int field, with initial value", element: &struct { Field []int `description:"field description"` }{ Field: []int{6, 3}, }, expected: []parser.Flat{{ Name: "field", Description: "field description", Default: "6, 3", }}, }, { desc: "map string field", element: &struct { Field map[string]string `description:"field description"` }{ Field: map[string]string{ parser.MapNamePlaceholder: "", }, }, expected: []parser.Flat{{ Name: "field.", Description: "field description", Default: "", }}, }, { desc: "struct pointer field", element: &struct { Foo *struct { Field string `description:"field description"` } `description:"foo description"` }{ Foo: &struct { Field string `description:"field description"` }{ Field: "test", }, }, expected: []parser.Flat{ { Name: "foo.field", Description: "field description", Default: "test", }, }, }, { desc: "struct pointer field, allow empty", element: &struct { Foo *struct { Field string `description:"field description"` } `description:"foo description" label:"allowEmpty"` }{ Foo: &struct { Field string `description:"field description"` }{ Field: "test", }, }, expected: []parser.Flat{ { Name: "foo", Description: "foo description", Default: "false", }, { Name: "foo.field", Description: "field description", Default: "test", }, }, }, { desc: "struct pointer field level 2", element: &struct { Foo *struct { Fii *struct { Field string `description:"field description"` } `description:"fii description"` } `description:"foo description"` }{ Foo: &struct { Fii *struct { Field string `description:"field description"` } `description:"fii description"` }{ Fii: &struct { Field string `description:"field description"` }{ Field: "test", }, }, }, expected: []parser.Flat{ { Name: "foo.fii.field", Description: "field description", Default: "test", }, }, }, { desc: "struct pointer field level 2, allow empty", element: &struct { Foo *struct { Fii *struct { Field string `description:"field description"` } `description:"fii description" label:"allowEmpty"` } `description:"foo description" label:"allowEmpty"` }{ Foo: &struct { Fii *struct { Field string `description:"field description"` } `description:"fii description" label:"allowEmpty"` }{ Fii: &struct { Field string `description:"field description"` }{ Field: "test", }, }, }, expected: []parser.Flat{ { Name: "foo", Description: "foo description", Default: "false", }, { Name: "foo.fii", Description: "fii description", Default: "false", }, { Name: "foo.fii.field", Description: "field description", Default: "test", }, }, }, { desc: "map string field level 2", element: &struct { Foo *struct { Fii map[string]string `description:"fii description"` } `description:"foo description"` }{ Foo: &struct { Fii map[string]string `description:"fii description"` }{ Fii: map[string]string{ parser.MapNamePlaceholder: "", }, }, }, expected: []parser.Flat{ { Name: "foo.fii.", Description: "fii description", Default: "", }, }, }, { desc: "map string pointer field level 2", element: &struct { Foo *struct { Fii map[string]*string `description:"fii description"` } `description:"foo description"` }{ Foo: &struct { Fii map[string]*string `description:"fii description"` }{ Fii: map[string]*string{ parser.MapNamePlaceholder: func(v string) *string { return &v }(""), }, }, }, expected: []parser.Flat{ { Name: "foo.fii.", Description: "fii description", Default: "", }, }, }, { desc: "map struct level 1", element: &struct { Foo map[string]struct { Field string `description:"field description"` Yo int `description:"yo description"` } `description:"foo description"` }{}, expected: []parser.Flat{ { Name: "foo.", Description: "foo description", Default: "false", }, { Name: "foo..field", Description: "field description", Default: "", }, { Name: "foo..yo", Description: "yo description", Default: "0", }, }, }, { desc: "map struct pointer level 1", element: &struct { Foo map[string]*struct { Field string `description:"field description"` Yo string `description:"yo description"` } `description:"foo description"` }{}, expected: []parser.Flat{ { Name: "foo.", Description: "foo description", Default: "false", }, { Name: "foo..field", Description: "field description", Default: "", }, { Name: "foo..yo", Description: "yo description", Default: "", }, }, }, { desc: "time duration field", element: &struct { Field time.Duration `description:"field description"` }{ Field: 1 * time.Second, }, expected: []parser.Flat{{ Name: "field", Description: "field description", Default: "1s", }}, }, { desc: "time duration field map", element: &struct { Foo map[string]*struct { Field time.Duration `description:"field description"` } `description:"foo description"` }{ Foo: map[string]*struct { Field time.Duration `description:"field description"` }{}, }, expected: []parser.Flat{ { Name: "foo.", Description: "foo description", Default: "false", }, { Name: "foo..field", Description: "field description", Default: "0s", }, }, }, { desc: "time duration field map 2", element: &struct { Foo map[string]*struct { Fii *struct { Field time.Duration `description:"field description"` } } `description:"foo description"` }{ Foo: map[string]*struct { Fii *struct { Field time.Duration `description:"field description"` } }{}, }, expected: []parser.Flat{ { Name: "foo.", Description: "foo description", Default: "false", }, { Name: "foo..fii.field", Description: "field description", Default: "0s", }, }, }, { desc: "time duration field 2", element: &struct { Foo *struct { Field time.Duration `description:"field description"` } }{ Foo: &struct { Field time.Duration `description:"field description"` }{ Field: 1 * time.Second, }, }, expected: []parser.Flat{{ Name: "foo.field", Description: "field description", Default: "1s", }}, }, { desc: "time duration field 3", element: &struct { Foo *struct { Fii *struct { Field time.Duration `description:"field description"` } } }{ Foo: &struct { Fii *struct { Field time.Duration `description:"field description"` } }{ Fii: &struct { Field time.Duration `description:"field description"` }{ Field: 1 * time.Second, }, }, }, expected: []parser.Flat{{ Name: "foo.fii.field", Description: "field description", Default: "1s", }}, }, { desc: "time duration field", element: &struct { Field types.Duration `description:"field description"` }{ Field: types.Duration(180 * time.Second), }, expected: []parser.Flat{{ Name: "field", Description: "field description", Default: "180", }}, }, { desc: "slice of struct", element: &struct { Foo *struct { Fii []struct { Field1 string `description:"field1 description"` Field2 int `description:"field2 description"` } `description:"fii description"` } `description:"foo description"` }{}, expected: []parser.Flat{ { Name: "foo.fii", Description: "fii description", Default: "", }, { Name: "foo.fii[0].field1", Description: "field1 description", Default: "", }, { Name: "foo.fii[0].field2", Description: "field2 description", Default: "0", }, }, }, // Skipped: because realistically not needed in Traefik for now. // { // desc: "map of map field level 2", // element: &struct { // Foo *struct { // Fii map[string]map[string]string `description:"fii description"` // } `description:"foo description"` // }{ // Foo: &struct { // Fii map[string]map[string]string `description:"fii description"` // }{ // Fii: map[string]map[string]string{ // parser.MapNamePlaceholder: { // parser.MapNamePlaceholder: "test", // }, // }, // }, // }, // expected: `XXX`, // }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() generator.Generate(test.element) entries, err := Encode(test.element) require.NoError(t, err) assert.Equal(t, test.expected, entries) }) } } paerser-0.2.0/flag/flagparser.go000066400000000000000000000053051436254333400165520ustar00rootroot00000000000000package flag import ( "fmt" "reflect" "regexp" "strings" "github.com/traefik/paerser/parser" ) // Parse parses the command-line flag arguments into a map, // using the type information in element to discriminate whether a flag is supposed to be a bool, // and other such ambiguities. func Parse(args []string, element interface{}) (map[string]string, error) { f := flagSet{ flagTypes: getFlagTypes(element), args: args, values: make(map[string]string), keys: make(map[string]string), } for { seen, err := f.parseOne() if seen { continue } if err == nil { break } return nil, err } return f.values, nil } type flagSet struct { flagTypes map[string]reflect.Kind args []string values map[string]string keys map[string]string } func (f *flagSet) parseOne() (bool, error) { if len(f.args) == 0 { return false, nil } s := f.args[0] if len(s) < 2 || s[0] != '-' { return false, nil } numMinuses := 1 if s[1] == '-' { numMinuses++ if len(s) == 2 { // "--" terminates the flags f.args = f.args[1:] return false, nil } } name := s[numMinuses:] if len(name) == 0 || name[0] == '-' || name[0] == '=' { return false, fmt.Errorf("bad flag syntax: %s", s) } // it's a flag. does it have an argument? f.args = f.args[1:] hasValue := false value := "" for i := 1; i < len(name); i++ { // equals cannot be first if name[i] == '=' { value = name[i+1:] hasValue = true name = name[0:i] break } } if hasValue { f.setValue(name, value) return true, nil } flagType := f.getFlagType(name) if flagType == reflect.Bool || flagType == reflect.Pointer { f.setValue(name, "true") return true, nil } if len(f.args) > 0 { // value is the next arg hasValue = true value, f.args = f.args[0], f.args[1:] } if !hasValue { return false, fmt.Errorf("flag needs an argument: -%s", name) } f.setValue(name, value) return true, nil } func (f *flagSet) setValue(name, value string) { srcKey := parser.DefaultRootName + "." + name neutralKey := strings.ToLower(srcKey) key, ok := f.keys[neutralKey] if !ok { f.keys[neutralKey] = srcKey key = srcKey } v, ok := f.values[key] if ok && f.getFlagType(name) == reflect.Slice { f.values[key] = v + "," + value return } f.values[key] = value } func (f *flagSet) getFlagType(name string) reflect.Kind { neutral := strings.ToLower(name) kind, ok := f.flagTypes[neutral] if ok { return kind } for n, k := range f.flagTypes { if strings.Contains(n, parser.MapNamePlaceholder) { p := strings.NewReplacer(".", `\.`, parser.MapNamePlaceholder, `([^.]+)`).Replace(n) if regexp.MustCompile(p).MatchString(neutral) { return k } } } return reflect.Invalid } paerser-0.2.0/flag/flagparser_test.go000066400000000000000000000157711436254333400176210ustar00rootroot00000000000000package flag import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestParse(t *testing.T) { testCases := []struct { desc string args []string element interface{} expected map[string]string }{ { desc: "no args", args: nil, expected: map[string]string{}, }, { desc: "bool value", args: []string{"--foo"}, element: &struct { Foo bool }{}, expected: map[string]string{ "traefik.foo": "true", }, }, { desc: "bool value capitalized", args: []string{"--Foo"}, element: &struct { Foo bool }{}, expected: map[string]string{ "traefik.Foo": "true", }, }, { desc: "equal", args: []string{"--foo=bar"}, element: &struct { Foo string }{}, expected: map[string]string{ "traefik.foo": "bar", }, }, { desc: "equal", args: []string{"--Foo=Bar"}, element: &struct { Foo string }{}, expected: map[string]string{ "traefik.Foo": "Bar", }, }, { desc: "space separated", args: []string{"--foo", "bar"}, element: &struct { Foo string }{}, expected: map[string]string{ "traefik.foo": "bar", }, }, { desc: "space separated capitalized", args: []string{"--Foo", "Bar"}, element: &struct { Foo string }{}, expected: map[string]string{ "traefik.Foo": "Bar", }, }, { desc: "space separated with end of parameter", args: []string{"--foo=bir", "--", "--bar"}, element: &struct { Foo string }{}, expected: map[string]string{ "traefik.foo": "bir", }, }, { desc: "multiple bool flags without value", args: []string{"--foo", "--bar"}, element: &struct { Foo bool Bar bool }{}, expected: map[string]string{ "traefik.foo": "true", "traefik.bar": "true", }, }, { desc: "slice with several flags", args: []string{"--foo=bar", "--foo=baz"}, element: &struct { Foo []string }{}, expected: map[string]string{ "traefik.foo": "bar,baz", }, }, { desc: "map string", args: []string{"--foo.name=bar"}, element: &struct { Foo map[string]string }{}, expected: map[string]string{ "traefik.foo.name": "bar", }, }, { desc: "map string capitalized", args: []string{"--foo.Name=Bar"}, element: &struct { Foo map[string]string }{}, expected: map[string]string{ "traefik.foo.Name": "Bar", }, }, { desc: "map struct", args: []string{"--foo.name.value=bar"}, element: &struct { Foo map[string]struct{ Value string } }{}, expected: map[string]string{ "traefik.foo.name.value": "bar", }, }, { desc: "map struct with sub-struct", args: []string{"--foo.name.bar.value=bar"}, element: &struct { Foo map[string]struct { Bar *struct{ Value string } } }{}, expected: map[string]string{ "traefik.foo.name.bar.value": "bar", }, }, { desc: "map struct with sub-map", args: []string{"--foo.name1.bar.name2.value=bar"}, element: &struct { Foo map[string]struct { Bar map[string]struct{ Value string } } }{}, expected: map[string]string{ "traefik.foo.name1.bar.name2.value": "bar", }, }, { desc: "slice with several flags 2", args: []string{"--foo", "bar", "--foo", "baz"}, element: &struct { Foo []string }{}, expected: map[string]string{ "traefik.foo": "bar,baz", }, }, { desc: "slice with several flags 3", args: []string{"--foo", "bar", "--foo=", "--baz"}, element: &struct { Foo []string Baz bool }{}, expected: map[string]string{ "traefik.foo": "bar,", "traefik.baz": "true", }, }, { desc: "slice with several flags 4", args: []string{"--foo", "bar", "--foo", "--baz"}, element: &struct { Foo []string Baz bool }{}, expected: map[string]string{ "traefik.foo": "bar,--baz", }, }, { desc: "multiple string flag", element: &struct { Foo string }{}, args: []string{"--foo=bar", "--foo=baz"}, expected: map[string]string{ "traefik.foo": "baz", }, }, { desc: "multiple string flag 2", element: &struct { Foo string }{}, args: []string{"--foo", "bar", "--foo", "baz"}, expected: map[string]string{ "traefik.foo": "baz", }, }, { desc: "string without value", element: &struct { Foo string Bar bool }{}, args: []string{"--foo", "--bar"}, expected: map[string]string{ "traefik.foo": "--bar", }, }, { desc: "struct pointer value", args: []string{"--foo"}, element: &struct { Foo *struct{ Field string } }{}, expected: map[string]string{ "traefik.foo": "true", }, }, { desc: "map string case sensitive", args: []string{"--foo.caseSensitiveName=barBoo"}, element: &struct { Foo map[string]string }{}, expected: map[string]string{ "traefik.foo.caseSensitiveName": "barBoo", }, }, { desc: "map struct with sub-map case-sensitive", args: []string{"--foo.Name1.bar.name2.value=firstValue", "--foo.naMe1.bar.name2.value=secondValue"}, element: &struct { Foo map[string]struct { Bar map[string]struct{ Value string } } }{}, expected: map[string]string{ "traefik.foo.Name1.bar.name2.value": "secondValue", }, }, { desc: "map struct with sub-map and different case", args: []string{"--foo.Name1.bar.name2.value=firstValue", "--foo.naMe1.bar.name2.value=secondValue"}, element: &struct { Foo map[string]struct { Bar map[string]struct{ Value string } } }{}, expected: map[string]string{ "traefik.foo.Name1.bar.name2.value": "secondValue", }, }, { desc: "pointer of struct and map without explicit value", args: []string{"--foo.default.bar.fuu"}, element: &struct { Foo map[string]struct { Bar *struct { Fuu *struct{ Value string } } } }{}, expected: map[string]string{ "traefik.foo.default.bar.fuu": "true", }, }, { desc: "slice with several flags 2 and different cases.", args: []string{"--foo", "bar", "--Foo", "baz"}, element: &struct { Foo []string }{}, expected: map[string]string{ "traefik.foo": "bar,baz", }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() fl, err := Parse(test.args, test.element) require.NoError(t, err) assert.Equal(t, test.expected, fl) }) } } func TestParse_Errors(t *testing.T) { testCases := []struct { desc string args []string element interface{} }{ { desc: "triple hyphen", args: []string{"---foo"}, element: &struct { Foo bool }{}, }, { desc: "equal", args: []string{"--=foo"}, element: &struct { Foo bool }{}, }, { desc: "string without value", element: &struct { Foo string Bar bool }{}, args: []string{"--foo"}, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() _, err := Parse(test.args, test.element) require.Error(t, err) }) } } paerser-0.2.0/flag/flagtype.go000066400000000000000000000021741436254333400162400ustar00rootroot00000000000000package flag import ( "reflect" "strings" "github.com/traefik/paerser/parser" ) func getFlagTypes(element interface{}) map[string]reflect.Kind { ref := map[string]reflect.Kind{} if element == nil { return ref } tp := reflect.TypeOf(element).Elem() addFlagType(ref, "", tp) return ref } func addFlagType(ref map[string]reflect.Kind, name string, typ reflect.Type) { switch typ.Kind() { case reflect.Bool, reflect.Slice: ref[name] = typ.Kind() case reflect.Map: addFlagType(ref, getName(name, parser.MapNamePlaceholder), typ.Elem()) case reflect.Pointer: if typ.Elem().Kind() == reflect.Struct { ref[name] = typ.Kind() } addFlagType(ref, name, typ.Elem()) case reflect.Struct: for j := 0; j < typ.NumField(); j++ { subField := typ.Field(j) if !parser.IsExported(subField) { continue } if subField.Anonymous { addFlagType(ref, getName(name), subField.Type) } else { addFlagType(ref, getName(name, subField.Name), subField.Type) } } default: // noop } } func getName(names ...string) string { return strings.TrimPrefix(strings.ToLower(strings.Join(names, ".")), ".") } paerser-0.2.0/flag/flagtype_test.go000066400000000000000000000101361436254333400172740ustar00rootroot00000000000000package flag import ( "reflect" "testing" "github.com/stretchr/testify/assert" "github.com/traefik/paerser/parser" ) func Test_getFlagTypes(t *testing.T) { testCases := []struct { desc string element interface{} expected map[string]reflect.Kind }{ { desc: "nil", element: nil, expected: map[string]reflect.Kind{}, }, { desc: "no fields", element: &struct{}{}, expected: map[string]reflect.Kind{}, }, { desc: "string field", element: &struct { Foo string }{}, expected: map[string]reflect.Kind{}, }, { desc: "bool field level 0", element: &struct { Foo bool fii bool }{}, expected: map[string]reflect.Kind{ "foo": reflect.Bool, }, }, { desc: "bool field level 1", element: &struct { Foo struct { Field bool } }{}, expected: map[string]reflect.Kind{ "foo.field": reflect.Bool, }, }, { desc: "bool field level 2", element: &struct { Foo *struct { Fii *struct { Field bool } } }{}, expected: map[string]reflect.Kind{ "foo": reflect.Pointer, "foo.fii": reflect.Pointer, "foo.fii.field": reflect.Bool, }, }, { desc: "pointer field", element: &struct { Foo *struct { Field string } }{}, expected: map[string]reflect.Kind{ "foo": reflect.Pointer, }, }, { desc: "bool field level 3", element: &struct { Foo *struct { Fii *struct { Fuu *struct { Field bool } } } }{}, expected: map[string]reflect.Kind{ "foo": reflect.Pointer, "foo.fii": reflect.Pointer, "foo.fii.fuu": reflect.Pointer, "foo.fii.fuu.field": reflect.Bool, }, }, { desc: "map string", element: &struct { Foo map[string]string }{}, expected: map[string]reflect.Kind{}, }, { desc: "map bool", element: &struct { Fii struct{} Foo map[string]bool }{}, expected: map[string]reflect.Kind{ "foo." + parser.MapNamePlaceholder: reflect.Bool, }, }, { desc: "map struct", element: &struct { Foo map[string]struct { Field bool } }{}, expected: map[string]reflect.Kind{ "foo." + parser.MapNamePlaceholder + ".field": reflect.Bool, }, }, { desc: "map of map bool", element: &struct { Foo map[string]map[string]bool }{}, expected: map[string]reflect.Kind{ "foo." + parser.MapNamePlaceholder + "." + parser.MapNamePlaceholder: reflect.Bool, }, }, { desc: "map struct map", element: &struct { Foo map[string]struct { Fii map[string]bool } }{}, expected: map[string]reflect.Kind{ "foo." + parser.MapNamePlaceholder + ".fii." + parser.MapNamePlaceholder: reflect.Bool, }, }, { desc: "pointer bool field level 0", element: &struct { Foo *bool }{}, expected: map[string]reflect.Kind{ "foo": reflect.Bool, }, }, { desc: "pointer int field level 0", element: &struct { Foo *int }{}, expected: map[string]reflect.Kind{}, }, { desc: "bool slice field level 0", element: &struct { Foo []bool }{}, expected: map[string]reflect.Kind{ "foo": reflect.Slice, }, }, { desc: "string slice field level 0", element: &struct { Foo []string }{}, expected: map[string]reflect.Kind{ "foo": reflect.Slice, }, }, { desc: "slice field level 1", element: &struct { Foo struct { Field []string } }{}, expected: map[string]reflect.Kind{ "foo.field": reflect.Slice, }, }, { desc: "map slice string", element: &struct { Foo map[string][]string }{}, expected: map[string]reflect.Kind{ "foo." + parser.MapNamePlaceholder: reflect.Slice, }, }, { desc: "embedded struct", element: &struct { Yo }{}, expected: map[string]reflect.Kind{ "foo": reflect.Bool, }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() actual := getFlagTypes(test.element) assert.Equal(t, test.expected, actual) }) } } type Yo struct { Foo bool } paerser-0.2.0/generator/000077500000000000000000000000001436254333400151475ustar00rootroot00000000000000paerser-0.2.0/generator/generator.go000066400000000000000000000041001436254333400174570ustar00rootroot00000000000000// Package generator implements the custom initialization of all the fields of an empty interface. package generator import ( "reflect" "github.com/traefik/paerser/parser" ) type initializer interface { SetDefaults() } // Generate recursively initializes an empty structure, calling SetDefaults on each field, when it applies. func Generate(element interface{}) { if element == nil { return } generate(element) } func generate(element interface{}) { field := reflect.ValueOf(element) fill(field) } func fill(field reflect.Value) { switch field.Kind() { case reflect.Pointer: setPtr(field) case reflect.Struct: setStruct(field) case reflect.Map: setMap(field) case reflect.Slice: if field.Type().Elem().Kind() == reflect.Struct || field.Type().Elem().Kind() == reflect.Pointer && field.Type().Elem().Elem().Kind() == reflect.Struct { slice := reflect.MakeSlice(field.Type(), 1, 1) field.Set(slice) // use Ptr to allow "SetDefaults" value := reflect.New(reflect.PointerTo(field.Type().Elem())) setPtr(value) elem := value.Elem().Elem() field.Index(0).Set(elem) } else if field.Len() == 0 { slice := reflect.MakeSlice(field.Type(), 0, 0) field.Set(slice) } } } func setPtr(field reflect.Value) { if field.IsNil() { field.Set(reflect.New(field.Type().Elem())) } if field.Type().Implements(reflect.TypeOf((*initializer)(nil)).Elem()) { method := field.MethodByName("SetDefaults") if method.IsValid() { method.Call([]reflect.Value{}) } } fill(field.Elem()) } func setStruct(field reflect.Value) { for i := 0; i < field.NumField(); i++ { fd := field.Field(i) structField := field.Type().Field(i) if structField.Tag.Get(parser.TagLabel) == "-" { continue } if parser.IsExported(structField) { fill(fd) } } } func setMap(field reflect.Value) { if field.IsNil() { field.Set(reflect.MakeMap(field.Type())) } ptrValue := reflect.New(reflect.PointerTo(field.Type().Elem())) fill(ptrValue) value := ptrValue.Elem().Elem() key := reflect.ValueOf(parser.MapNamePlaceholder) field.SetMapIndex(key, value) } paerser-0.2.0/generator/generator_test.go000066400000000000000000000220511436254333400205230ustar00rootroot00000000000000package generator import ( "testing" "github.com/stretchr/testify/assert" "github.com/traefik/paerser/parser" ) func TestGenerate(t *testing.T) { testCases := []struct { desc string element interface{} expected interface{} }{ { desc: "nil", }, { desc: "simple", element: &Ya{}, expected: &Ya{ Foo: &Yaa{ FieldIn1: "", FieldIn2: false, FieldIn3: 0, FieldIn4: map[string]string{ parser.MapNamePlaceholder: "", }, FieldIn5: map[string]int{ parser.MapNamePlaceholder: 0, }, FieldIn6: map[string]struct{ Field string }{ parser.MapNamePlaceholder: {}, }, FieldIn7: map[string]struct{ Field map[string]string }{ parser.MapNamePlaceholder: { Field: map[string]string{ parser.MapNamePlaceholder: "", }, }, }, FieldIn8: map[string]*struct{ Field string }{ parser.MapNamePlaceholder: {}, }, FieldIn9: map[string]*struct{ Field map[string]string }{ parser.MapNamePlaceholder: { Field: map[string]string{ parser.MapNamePlaceholder: "", }, }, }, FieldIn10: struct{ Field string }{}, FieldIn11: &struct{ Field string }{}, FieldIn12: func(v string) *string { return &v }(""), FieldIn13: func(v bool) *bool { return &v }(false), FieldIn14: func(v int) *int { return &v }(0), }, Field1: "", Field2: false, Field3: 0, Field4: map[string]string{ parser.MapNamePlaceholder: "", }, Field5: map[string]int{ parser.MapNamePlaceholder: 0, }, Field6: map[string]struct{ Field string }{ parser.MapNamePlaceholder: {}, }, Field7: map[string]struct{ Field map[string]string }{ parser.MapNamePlaceholder: { Field: map[string]string{ parser.MapNamePlaceholder: "", }, }, }, Field8: map[string]*struct{ Field string }{ parser.MapNamePlaceholder: {}, }, Field9: map[string]*struct{ Field map[string]string }{ parser.MapNamePlaceholder: { Field: map[string]string{ parser.MapNamePlaceholder: "", }, }, }, Field10: struct{ Field string }{}, Field11: &struct{ Field string }{}, Field12: func(v string) *string { return &v }(""), Field13: func(v bool) *bool { return &v }(false), Field14: func(v int) *int { return &v }(0), Field15: []int{}, }, }, { desc: "with initial state", element: &Ya{ Foo: &Yaa{ FieldIn1: "bar", FieldIn2: false, FieldIn3: 1, FieldIn4: nil, FieldIn5: nil, FieldIn6: nil, FieldIn7: nil, FieldIn8: nil, FieldIn9: nil, FieldIn10: struct{ Field string }{}, FieldIn11: nil, FieldIn12: nil, FieldIn13: nil, FieldIn14: nil, }, Field1: "bir", Field2: true, Field3: 0, Field4: nil, Field5: nil, Field6: nil, Field7: nil, Field8: nil, Field9: nil, Field10: struct{ Field string }{}, Field11: nil, Field12: nil, Field13: nil, Field14: nil, Field15: []int{7}, }, expected: &Ya{ Foo: &Yaa{ FieldIn1: "bar", FieldIn2: false, FieldIn3: 1, FieldIn4: map[string]string{ parser.MapNamePlaceholder: "", }, FieldIn5: map[string]int{ parser.MapNamePlaceholder: 0, }, FieldIn6: map[string]struct{ Field string }{ parser.MapNamePlaceholder: {}, }, FieldIn7: map[string]struct{ Field map[string]string }{ parser.MapNamePlaceholder: { Field: map[string]string{ parser.MapNamePlaceholder: "", }, }, }, FieldIn8: map[string]*struct{ Field string }{ parser.MapNamePlaceholder: {}, }, FieldIn9: map[string]*struct{ Field map[string]string }{ parser.MapNamePlaceholder: { Field: map[string]string{ parser.MapNamePlaceholder: "", }, }, }, FieldIn10: struct{ Field string }{}, FieldIn11: &struct{ Field string }{}, FieldIn12: func(v string) *string { return &v }(""), FieldIn13: func(v bool) *bool { return &v }(false), FieldIn14: func(v int) *int { return &v }(0), }, Field1: "bir", Field2: true, Field3: 0, Field4: map[string]string{ parser.MapNamePlaceholder: "", }, Field5: map[string]int{ parser.MapNamePlaceholder: 0, }, Field6: map[string]struct{ Field string }{ parser.MapNamePlaceholder: {}, }, Field7: map[string]struct{ Field map[string]string }{ parser.MapNamePlaceholder: { Field: map[string]string{ parser.MapNamePlaceholder: "", }, }, }, Field8: map[string]*struct{ Field string }{ parser.MapNamePlaceholder: {}, }, Field9: map[string]*struct{ Field map[string]string }{ parser.MapNamePlaceholder: { Field: map[string]string{ parser.MapNamePlaceholder: "", }, }, }, Field10: struct{ Field string }{}, Field11: &struct{ Field string }{}, Field12: func(v string) *string { return &v }(""), Field13: func(v bool) *bool { return &v }(false), Field14: func(v int) *int { return &v }(0), Field15: []int{7}, }, }, { desc: "setDefault", element: &Hu{}, expected: &Hu{ Foo: "hu", Fii: &Hi{ Field: "hi", }, Fuu: map[string]string{"": ""}, Fee: map[string]Hi{"": {Field: "hi"}}, }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() Generate(test.element) assert.Equal(t, test.expected, test.element) }) } } func Test_generate(t *testing.T) { testCases := []struct { desc string element interface{} expected interface{} }{ { desc: "struct pointer", element: &struct { Foo string Fii *struct{ Field string } }{}, expected: &struct { Foo string Fii *struct{ Field string } }{ Foo: "", Fii: &struct{ Field string }{ Field: "", }, }, }, { desc: "string slice", element: &struct { Foo []string }{}, expected: &struct { Foo []string }{ Foo: []string{}, }, }, { desc: "int slice", element: &struct { Foo []int }{}, expected: &struct { Foo []int }{ Foo: []int{}, }, }, { desc: "struct slice", element: &struct { Foo []struct { Field string } }{}, expected: &struct { Foo []struct { Field string } }{ Foo: []struct { Field string }{ {Field: ""}, }, }, }, { desc: "map string", element: &struct { Foo string Fii map[string]string }{}, expected: &struct { Foo string Fii map[string]string }{ Foo: "", Fii: map[string]string{ parser.MapNamePlaceholder: "", }, }, }, { desc: "map struct", element: &struct { Foo string Fii map[string]struct{ Field string } }{}, expected: &struct { Foo string Fii map[string]struct{ Field string } }{ Foo: "", Fii: map[string]struct{ Field string }{ parser.MapNamePlaceholder: {}, }, }, }, { desc: "map struct pointer level 2", element: &struct { Foo string Fuu *struct { Fii map[string]*struct{ Field string } } }{}, expected: &struct { Foo string Fuu *struct { Fii map[string]*struct{ Field string } } }{ Foo: "", Fuu: &struct { Fii map[string]*struct { Field string } }{ Fii: map[string]*struct{ Field string }{ parser.MapNamePlaceholder: { Field: "", }, }, }, }, }, { desc: "SetDefaults", element: &Hu{}, expected: &Hu{ Foo: "hu", Fii: &Hi{ Field: "hi", }, Fuu: map[string]string{ parser.MapNamePlaceholder: "", }, Fee: map[string]Hi{ parser.MapNamePlaceholder: { Field: "hi", }, }, }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() generate(test.element) assert.Equal(t, test.expected, test.element) }) } } type Hu struct { Foo string Fii *Hi Fuu map[string]string Fee map[string]Hi } func (h *Hu) SetDefaults() { h.Foo = "hu" } type Hi struct { Field string } func (h *Hi) SetDefaults() { h.Field = "hi" } type Ya struct { Foo *Yaa Field1 string Field2 bool Field3 int Field4 map[string]string Field5 map[string]int Field6 map[string]struct{ Field string } Field7 map[string]struct{ Field map[string]string } Field8 map[string]*struct{ Field string } Field9 map[string]*struct{ Field map[string]string } Field10 struct{ Field string } Field11 *struct{ Field string } Field12 *string Field13 *bool Field14 *int Field15 []int } type Yaa struct { FieldIn1 string FieldIn2 bool FieldIn3 int FieldIn4 map[string]string FieldIn5 map[string]int FieldIn6 map[string]struct{ Field string } FieldIn7 map[string]struct{ Field map[string]string } FieldIn8 map[string]*struct{ Field string } FieldIn9 map[string]*struct{ Field map[string]string } FieldIn10 struct{ Field string } FieldIn11 *struct{ Field string } FieldIn12 *string FieldIn13 *bool FieldIn14 *int } paerser-0.2.0/go.mod000066400000000000000000000014631436254333400142730ustar00rootroot00000000000000module github.com/traefik/paerser go 1.19 require ( github.com/BurntSushi/toml v1.2.1 github.com/Masterminds/sprig/v3 v3.2.3 github.com/stretchr/testify v1.8.1 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/uuid v1.1.1 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.11 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/spf13/cast v1.3.1 // indirect golang.org/x/crypto v0.3.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) paerser-0.2.0/go.sum000066400000000000000000000151571436254333400143250ustar00rootroot00000000000000github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= paerser-0.2.0/parser/000077500000000000000000000000001436254333400144555ustar00rootroot00000000000000paerser-0.2.0/parser/element_fill.go000066400000000000000000000327221436254333400174510ustar00rootroot00000000000000package parser import ( "fmt" "math" "reflect" "strconv" "strings" "time" "github.com/traefik/paerser/types" ) const defaultRawSliceSeparator = "," type initializer interface { SetDefaults() } // FillerOpts Options for the filler. type FillerOpts struct { AllowSliceAsStruct bool RawSliceSeparator string } // Fill populates the fields of the element using the information in node. func Fill(element interface{}, node *Node, opts FillerOpts) error { return newFiller(opts).Fill(element, node) } type filler struct { FillerOpts } func newFiller(opts FillerOpts) filler { if opts.RawSliceSeparator == "" { opts.RawSliceSeparator = defaultRawSliceSeparator } return filler{FillerOpts: opts} } // Fill populates the fields of the element using the information in node. func (f filler) Fill(element interface{}, node *Node) error { if element == nil || node == nil { return nil } if node.Kind == 0 { return fmt.Errorf("missing node type: %s", node.Name) } root := reflect.ValueOf(element) if root.Kind() == reflect.Struct { return fmt.Errorf("struct are not supported, use pointer instead") } return f.fill(root.Elem(), node) } func (f filler) fill(field reflect.Value, node *Node) error { // related to allow-empty or ignore tag if node.Disabled { return nil } switch field.Kind() { case reflect.String: field.SetString(node.Value) return nil case reflect.Bool: val, err := strconv.ParseBool(node.Value) if err != nil { return err } field.SetBool(val) return nil case reflect.Int8: return setInt(field, node.Value, 8) case reflect.Int16: return setInt(field, node.Value, 16) case reflect.Int32: return setInt(field, node.Value, 32) case reflect.Int64, reflect.Int: return setInt(field, node.Value, 64) case reflect.Uint8: return setUint(field, node.Value, 8) case reflect.Uint16: return setUint(field, node.Value, 16) case reflect.Uint32: return setUint(field, node.Value, 32) case reflect.Uint64, reflect.Uint: return setUint(field, node.Value, 64) case reflect.Float32: return setFloat(field, node.Value, 32) case reflect.Float64: return setFloat(field, node.Value, 64) case reflect.Struct: return f.setStruct(field, node) case reflect.Pointer: return f.setPtr(field, node) case reflect.Map: return f.setMap(field, node) case reflect.Slice: return f.setSlice(field, node) default: return nil } } func (f filler) setPtr(field reflect.Value, node *Node) error { if field.IsNil() { field.Set(reflect.New(field.Type().Elem())) if field.Type().Implements(reflect.TypeOf((*initializer)(nil)).Elem()) { method := field.MethodByName("SetDefaults") if method.IsValid() { method.Call([]reflect.Value{}) } } } return f.fill(field.Elem(), node) } func (f filler) setStruct(field reflect.Value, node *Node) error { for _, child := range node.Children { fd := field.FieldByName(child.FieldName) zeroValue := reflect.Value{} if fd == zeroValue { return fmt.Errorf("field not found, node: %s (%s)", child.Name, child.FieldName) } err := f.fill(fd, child) if err != nil { return err } } return nil } func (f filler) setSlice(field reflect.Value, node *Node) error { if field.Type().Elem().Kind() == reflect.Struct || field.Type().Elem().Kind() == reflect.Pointer && field.Type().Elem().Elem().Kind() == reflect.Struct { return f.setSliceStruct(field, node) } if len(node.Value) == 0 { return nil } values := strings.Split(node.Value, f.RawSliceSeparator) if f.RawSliceSeparator != defaultRawSliceSeparator { if len(values) < 2 { // TODO(ldez): must be changed to an error. return makeSlice(field, strings.Split(node.Value, defaultRawSliceSeparator)) } // TODO(ldez): this is related to raw map and file. Rethink the node parser. values = values[2:] } return makeSlice(field, values) } func makeSlice(field reflect.Value, values []string) error { slice := reflect.MakeSlice(field.Type(), len(values), len(values)) field.Set(slice) for i := 0; i < len(values); i++ { value := strings.TrimSpace(values[i]) switch field.Type().Elem().Kind() { case reflect.String: field.Index(i).SetString(value) case reflect.Int: val, err := strconv.ParseInt(value, 10, 64) if err != nil { return err } field.Index(i).SetInt(val) case reflect.Int8: err := setInt(field.Index(i), value, 8) if err != nil { return err } case reflect.Int16: err := setInt(field.Index(i), value, 16) if err != nil { return err } case reflect.Int32: err := setInt(field.Index(i), value, 32) if err != nil { return err } case reflect.Int64: err := setInt(field.Index(i), value, 64) if err != nil { return err } case reflect.Uint: val, err := strconv.ParseUint(value, 10, 64) if err != nil { return err } field.Index(i).SetUint(val) case reflect.Uint8: err := setUint(field.Index(i), value, 8) if err != nil { return err } case reflect.Uint16: err := setUint(field.Index(i), value, 16) if err != nil { return err } case reflect.Uint32: err := setUint(field.Index(i), value, 32) if err != nil { return err } case reflect.Uint64: err := setUint(field.Index(i), value, 64) if err != nil { return err } case reflect.Float32: err := setFloat(field.Index(i), value, 32) if err != nil { return err } case reflect.Float64: err := setFloat(field.Index(i), value, 64) if err != nil { return err } case reflect.Bool: val, err := strconv.ParseBool(value) if err != nil { return err } field.Index(i).SetBool(val) default: return fmt.Errorf("unsupported type: %s", field.Type().Elem()) } } return nil } func (f filler) setSliceStruct(field reflect.Value, node *Node) error { if f.AllowSliceAsStruct && node.Tag.Get(TagLabelSliceAsStruct) != "" { return f.setSliceAsStruct(field, node) } field.Set(reflect.MakeSlice(field.Type(), len(node.Children), len(node.Children))) for i, child := range node.Children { // use Ptr to allow "SetDefaults" value := reflect.New(reflect.PointerTo(field.Type().Elem())) err := f.setPtr(value, child) if err != nil { return err } field.Index(i).Set(value.Elem().Elem()) } return nil } func (f filler) setSliceAsStruct(field reflect.Value, node *Node) error { if len(node.Children) == 0 { return fmt.Errorf("invalid slice: node %s", node.Name) } // use Ptr to allow "SetDefaults" value := reflect.New(reflect.PointerTo(field.Type().Elem())) if err := f.setPtr(value, node); err != nil { return err } elem := value.Elem().Elem() field.Set(reflect.MakeSlice(field.Type(), 1, 1)) field.Index(0).Set(elem) return nil } func (f filler) setMap(field reflect.Value, node *Node) error { if field.IsNil() { field.Set(reflect.MakeMap(field.Type())) } if field.Type().Elem().Kind() == reflect.Interface { err := f.fillRawValue(field, node, false) if err != nil { return err } for _, child := range node.Children { err = f.fillRawValue(field, child, true) if err != nil { return err } } return nil } for _, child := range node.Children { ptrValue := reflect.New(reflect.PointerTo(field.Type().Elem())) err := f.fill(ptrValue, child) if err != nil { return err } value := ptrValue.Elem().Elem() key := reflect.ValueOf(child.Name) field.SetMapIndex(key, value) } return nil } func setInt(field reflect.Value, value string, bitSize int) error { switch field.Type() { case reflect.TypeOf(types.Duration(0)): return setDuration(field, value, bitSize, time.Second) case reflect.TypeOf(time.Duration(0)): return setDuration(field, value, bitSize, time.Nanosecond) default: val, err := strconv.ParseInt(value, 10, bitSize) if err != nil { return err } field.Set(reflect.ValueOf(val).Convert(field.Type())) return nil } } func setDuration(field reflect.Value, value string, bitSize int, defaultUnit time.Duration) error { val, err := strconv.ParseInt(value, 10, bitSize) if err == nil { field.Set(reflect.ValueOf(time.Duration(val) * defaultUnit).Convert(field.Type())) return nil } duration, err := time.ParseDuration(value) if err != nil { return err } field.Set(reflect.ValueOf(duration).Convert(field.Type())) return nil } func setUint(field reflect.Value, value string, bitSize int) error { val, err := strconv.ParseUint(value, 10, bitSize) if err != nil { return err } field.Set(reflect.ValueOf(val).Convert(field.Type())) return nil } func setFloat(field reflect.Value, value string, bitSize int) error { val, err := strconv.ParseFloat(value, bitSize) if err != nil { return err } field.Set(reflect.ValueOf(val).Convert(field.Type())) return nil } func (f filler) fillRawValue(field reflect.Value, node *Node, subMap bool) error { m, ok := node.RawValue.(map[string]interface{}) if !ok { return nil } if _, self := m[node.Name]; self || !subMap { for k, v := range m { if f.RawSliceSeparator == defaultRawSliceSeparator { field.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v)) continue } // TODO(ldez): all the next section is related to raw map and file. Rethink the node parser. s, ok := v.(string) if !ok || len(s) == 0 || !strings.HasPrefix(s, f.RawSliceSeparator) { rawValue, err := f.cleanRawValue(reflect.ValueOf(v)) if err != nil { return err } field.SetMapIndex(reflect.ValueOf(k), rawValue) continue } // typed slice value, err := f.fillRawTypedSlice(s) if err != nil { return err } field.SetMapIndex(reflect.ValueOf(k), value) } return nil } // In the case of sub-map, fill raw typed slices recursively. _, err := f.fillRawMapWithTypedSlice(m) if err != nil { return err } p := map[string]interface{}{node.Name: m} node.RawValue = p field.SetMapIndex(reflect.ValueOf(node.Name), reflect.ValueOf(p[node.Name])) return nil } func (f filler) fillRawMapWithTypedSlice(elt interface{}) (reflect.Value, error) { eltValue := reflect.ValueOf(elt) switch eltValue.Kind() { case reflect.String: if strings.HasPrefix(elt.(string), f.RawSliceSeparator) { sliceValue, err := f.fillRawTypedSlice(elt.(string)) if err != nil { return eltValue, err } return sliceValue, nil } case reflect.Map: for k, v := range elt.(map[string]interface{}) { value, err := f.fillRawMapWithTypedSlice(v) if err != nil { return eltValue, err } eltValue.SetMapIndex(reflect.ValueOf(k), value) } } return eltValue, nil } func (f filler) fillRawTypedSlice(s string) (reflect.Value, error) { raw := strings.Split(s, f.RawSliceSeparator) rawType, err := strconv.Atoi(raw[1]) if err != nil { return reflect.Value{}, err } kind := reflect.Kind(rawType) slice := reflect.MakeSlice(reflect.TypeOf([]interface{}{}), len(raw[2:]), len(raw[2:])) for i := 0; i < len(raw[2:]); i++ { switch kind { case reflect.Bool: val, err := strconv.ParseBool(raw[i+2]) if err != nil { return reflect.Value{}, fmt.Errorf("parse bool: %s, %w", raw[i+2], err) } slice.Index(i).Set(reflect.ValueOf(val)) case reflect.Int: val, err := strconv.ParseInt(raw[i+2], 10, 64) if err != nil { return reflect.Value{}, fmt.Errorf("parse int: %s, %w", raw[i+2], err) } slice.Index(i).Set(reflect.ValueOf(val)) case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: val, err := strconv.ParseInt(raw[i+2], 10, int(math.Pow(2, float64(kind-reflect.Int+2)))) if err != nil { return reflect.Value{}, fmt.Errorf("parse %s: %s, %w", kind, raw[i+2], err) } slice.Index(i).Set(reflect.ValueOf(val)) case reflect.Uint: val, err := strconv.ParseUint(raw[i+2], 10, 64) if err != nil { return reflect.Value{}, fmt.Errorf("parse uint: %s, %w", raw[i+2], err) } slice.Index(i).Set(reflect.ValueOf(val)) case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: val, err := strconv.ParseUint(raw[i+2], 10, int(math.Pow(2, float64(kind-reflect.Uint+2)))) if err != nil { return reflect.Value{}, fmt.Errorf("parse uint: %s, %w", raw[i+2], err) } slice.Index(i).Set(reflect.ValueOf(val)) case reflect.Float32: err := setFloat(slice.Index(i), raw[i+2], 32) if err != nil { return reflect.Value{}, fmt.Errorf("parse float32: %s, %w", raw[i+2], err) } case reflect.Float64: err := setFloat(slice.Index(i), raw[i+2], 64) if err != nil { return reflect.Value{}, fmt.Errorf("parse float64: %s, %w", raw[i+2], err) } case reflect.String: slice.Index(i).Set(reflect.ValueOf(raw[i+2])) default: return reflect.Value{}, fmt.Errorf("unsupported kind: %d", kind) } } return slice, nil } func (f filler) cleanRawValue(value reflect.Value) (reflect.Value, error) { switch value.Kind() { case reflect.Pointer: rawValue, err := f.cleanRawValue(value.Elem()) if err != nil { return reflect.Value{}, err } value.Elem().Set(rawValue) case reflect.Map: keys := value.MapKeys() for _, key := range keys { v := value.MapIndex(key) rawValue, err := f.cleanRawValue(v) if err != nil { return reflect.Value{}, err } value.SetMapIndex(key, rawValue) } case reflect.Slice: if value.IsZero() { return value, nil } for i := 0; i < value.Len(); i++ { if !value.Index(i).IsZero() { rawValue, err := f.cleanRawValue(value.Index(i)) if err != nil { return reflect.Value{}, err } value.Index(i).Set(rawValue) } } case reflect.Interface: return f.cleanRawValue(value.Elem()) case reflect.String: if strings.HasPrefix(value.String(), f.RawSliceSeparator) { return f.fillRawTypedSlice(value.String()) } } return value, nil } paerser-0.2.0/parser/element_fill_test.go000066400000000000000000001132411436254333400205040ustar00rootroot00000000000000package parser import ( "reflect" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/traefik/paerser/types" ) func TestFill(t *testing.T) { type expected struct { element interface{} error bool } testCases := []struct { desc string rawSliceSeparator string node *Node element interface{} expected expected }{ { desc: "empty node", node: &Node{}, element: &struct{ Foo string }{}, expected: expected{error: true}, }, { desc: "empty element", node: &Node{Name: "traefik", Kind: reflect.Struct}, element: &struct{}{}, expected: expected{element: &struct{}{}}, }, { desc: "type struct as root", node: &Node{Name: "traefik", Kind: reflect.Struct}, element: struct{}{}, expected: expected{error: true}, }, { desc: "nil node", node: nil, element: &struct{ Foo string }{}, expected: expected{element: &struct{ Foo string }{}}, }, { desc: "nil element", node: &Node{Name: "traefik", Kind: reflect.Struct}, element: nil, expected: expected{element: nil}, }, { desc: "string", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "bar", Kind: reflect.String}, }, }, element: &struct{ Foo string }{}, expected: expected{element: &struct{ Foo string }{Foo: "bar"}}, }, { desc: "field not found", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Fii", Value: "bar", Kind: reflect.String}, }, }, element: &struct{ Foo string }{}, expected: expected{error: true}, }, { desc: "2 children", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Fii", FieldName: "Fii", Value: "bir", Kind: reflect.String}, {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int}, }, }, element: &struct { Fii string Foo int }{}, expected: expected{element: &struct { Fii string Foo int }{Fii: "bir", Foo: 4}}, }, { desc: "case insensitive", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "foo", FieldName: "Foo", Value: "bir", Kind: reflect.String}, }, }, element: &struct { Foo string foo int }{}, expected: expected{element: &struct { Foo string foo int }{Foo: "bir"}}, }, { desc: "func", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Kind: reflect.Func}, }, }, element: &struct{ Foo func() }{}, expected: expected{element: &struct{ Foo func() }{}}, }, { desc: "int", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int}, }, }, element: &struct{ Foo int }{}, expected: expected{element: &struct{ Foo int }{Foo: 4}}, }, { desc: "invalid int", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Int}, }, }, element: &struct{ Foo int }{}, expected: expected{error: true}, }, { desc: "int8", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int8}, }, }, element: &struct{ Foo int8 }{}, expected: expected{element: &struct{ Foo int8 }{Foo: 4}}, }, { desc: "invalid int8", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Int8}, }, }, element: &struct{ Foo int8 }{}, expected: expected{error: true}, }, { desc: "int16", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int16}, }, }, element: &struct{ Foo int16 }{}, expected: expected{element: &struct{ Foo int16 }{Foo: 4}}, }, { desc: "invalid int16", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Int16}, }, }, element: &struct{ Foo int16 }{}, expected: expected{error: true}, }, { desc: "int32", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int32}, }, }, element: &struct{ Foo int32 }{}, expected: expected{element: &struct{ Foo int32 }{Foo: 4}}, }, { desc: "invalid int32", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Int32}, }, }, element: &struct{ Foo int32 }{}, expected: expected{error: true}, }, { desc: "int64", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int64}, }, }, element: &struct{ Foo int64 }{}, expected: expected{element: &struct{ Foo int64 }{Foo: 4}}, }, { desc: "invalid int64", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Int64}, }, }, element: &struct{ Foo int64 }{}, expected: expected{error: true}, }, { desc: "uint", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Uint}, }, }, element: &struct{ Foo uint }{}, expected: expected{element: &struct{ Foo uint }{Foo: 4}}, }, { desc: "invalid uint", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Uint}, }, }, element: &struct{ Foo uint }{}, expected: expected{error: true}, }, { desc: "uint8", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Uint8}, }, }, element: &struct{ Foo uint8 }{}, expected: expected{element: &struct{ Foo uint8 }{Foo: 4}}, }, { desc: "invalid uint8", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Uint8}, }, }, element: &struct{ Foo uint8 }{}, expected: expected{error: true}, }, { desc: "uint16", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Uint16}, }, }, element: &struct{ Foo uint16 }{}, expected: expected{element: &struct{ Foo uint16 }{Foo: 4}}, }, { desc: "invalid uint16", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Uint16}, }, }, element: &struct{ Foo uint16 }{}, expected: expected{error: true}, }, { desc: "uint32", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Uint32}, }, }, element: &struct{ Foo uint32 }{}, expected: expected{element: &struct{ Foo uint32 }{Foo: 4}}, }, { desc: "invalid uint32", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Uint32}, }, }, element: &struct{ Foo uint32 }{}, expected: expected{error: true}, }, { desc: "uint64", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Uint64}, }, }, element: &struct{ Foo uint64 }{}, expected: expected{element: &struct{ Foo uint64 }{Foo: 4}}, }, { desc: "invalid uint64", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Uint64}, }, }, element: &struct{ Foo uint64 }{}, expected: expected{error: true}, }, { desc: "time.Duration with unit", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4s", Kind: reflect.Int64}, }, }, element: &struct{ Foo time.Duration }{}, expected: expected{element: &struct{ Foo time.Duration }{Foo: 4 * time.Second}}, }, { desc: "time.Duration without unit", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int64}, }, }, element: &struct{ Foo time.Duration }{}, expected: expected{element: &struct{ Foo time.Duration }{Foo: 4 * time.Nanosecond}}, }, { desc: "types.Duration with unit", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4s", Kind: reflect.Int64}, }, }, element: &struct{ Foo types.Duration }{}, expected: expected{element: &struct{ Foo types.Duration }{Foo: types.Duration(4 * time.Second)}}, }, { desc: "types.Duration without unit", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int64}, }, }, element: &struct{ Foo types.Duration }{}, expected: expected{element: &struct{ Foo types.Duration }{Foo: types.Duration(4 * time.Second)}}, }, { desc: "bool", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "true", Kind: reflect.Bool}, }, }, element: &struct{ Foo bool }{}, expected: expected{element: &struct{ Foo bool }{Foo: true}}, }, { desc: "invalid bool", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "bool", Kind: reflect.Bool}, }, }, element: &struct{ Foo bool }{}, expected: expected{error: true}, }, { desc: "float32", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "2.1", Kind: reflect.Float32}, }, }, element: &struct{ Foo float32 }{}, expected: expected{element: &struct{ Foo float32 }{Foo: 2.1}}, }, { desc: "invalid float32", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "two dot one", Kind: reflect.Float32}, }, }, element: &struct{ Foo float32 }{}, expected: expected{error: true}, }, { desc: "float64", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "2.1", Kind: reflect.Float64}, }, }, element: &struct{ Foo float64 }{}, expected: expected{element: &struct{ Foo float64 }{Foo: 2.1}}, }, { desc: "invalid float64", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "two dot one", Kind: reflect.Float64}, }, }, element: &struct{ Foo float64 }{}, expected: expected{error: true}, }, { desc: "struct", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Foo", FieldName: "Foo", Kind: reflect.Struct, Children: []*Node{ {Name: "Fii", FieldName: "Fii", Value: "huu", Kind: reflect.String}, {Name: "Fuu", FieldName: "Fuu", Value: "6", Kind: reflect.Int}, }, }, }, }, element: &struct { Foo struct { Fii string Fuu int } }{}, expected: expected{ element: &struct { Foo struct { Fii string Fuu int } }{ Foo: struct { Fii string Fuu int }{ Fii: "huu", Fuu: 6, }, }, }, }, { desc: "pointer", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Foo", FieldName: "Foo", Kind: reflect.Struct, Children: []*Node{ {Name: "Fii", FieldName: "Fii", Value: "huu", Kind: reflect.String}, {Name: "Fuu", FieldName: "Fuu", Value: "6", Kind: reflect.Int}, }, }, }, }, element: &struct { Foo *struct { Fii string Fuu int } }{}, expected: expected{ element: &struct { Foo *struct { Fii string Fuu int } }{ Foo: &struct { Fii string Fuu int }{ Fii: "huu", Fuu: 6, }, }, }, }, { desc: "pointer disabled false without children", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Foo", FieldName: "Foo", Kind: reflect.Pointer, }, }, }, element: &struct { Foo *struct { Fii string Fuu int } `label:"allowEmpty"` }{}, expected: expected{ element: &struct { Foo *struct { Fii string Fuu int } `label:"allowEmpty"` }{ Foo: &struct { Fii string Fuu int }{}, }, }, }, { desc: "pointer disabled true without children", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Foo", FieldName: "Foo", Kind: reflect.Pointer, Disabled: true, }, }, }, element: &struct { Foo *struct { Fii string Fuu int } `label:"allowEmpty"` }{}, expected: expected{ element: &struct { Foo *struct { Fii string Fuu int } `label:"allowEmpty"` }{}, }, }, { desc: "pointer disabled true with children", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Foo", FieldName: "Foo", Disabled: true, Kind: reflect.Pointer, Children: []*Node{ {Name: "Fii", FieldName: "Fii", Value: "huu", Kind: reflect.String}, {Name: "Fuu", FieldName: "Fuu", Value: "6", Kind: reflect.Int}, }, }, }, }, element: &struct { Foo *struct { Fii string Fuu int } `label:"allowEmpty"` }{}, expected: expected{ element: &struct { Foo *struct { Fii string Fuu int } `label:"allowEmpty"` }{}, }, }, { desc: "map string", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Foo", FieldName: "Foo", Kind: reflect.Map, Children: []*Node{ {Name: "name1", Value: "hii", Kind: reflect.String}, {Name: "name2", Value: "huu", Kind: reflect.String}, }, }, }, }, element: &struct { Foo map[string]string }{}, expected: expected{ element: &struct { Foo map[string]string }{ Foo: map[string]string{ "name1": "hii", "name2": "huu", }, }, }, }, { desc: "map struct", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Foo", FieldName: "Foo", Kind: reflect.Map, Children: []*Node{ { Name: "name1", Kind: reflect.Struct, Children: []*Node{ {Name: "Fii", FieldName: "Fii", Kind: reflect.String, Value: "hii"}, }, }, { Name: "name2", Kind: reflect.Struct, Children: []*Node{ {Name: "Fii", FieldName: "Fii", Kind: reflect.String, Value: "huu"}, }, }, }, }, }, }, element: &struct { Foo map[string]struct{ Fii string } }{}, expected: expected{ element: &struct { Foo map[string]struct{ Fii string } }{ Foo: map[string]struct{ Fii string }{ "name1": {Fii: "hii"}, "name2": {Fii: "huu"}, }, }, }, }, { desc: "empty map", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Foo", FieldName: "Foo", Kind: reflect.Map, Children: []*Node{}, }, }, }, element: &struct { Foo map[string]string }{}, expected: expected{ element: &struct { Foo map[string]string }{ Foo: map[string]string{}, }, }, }, { desc: "slice string", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "huu,hii,hoo", Kind: reflect.Slice}, }, }, element: &struct{ Foo []string }{}, expected: expected{element: &struct{ Foo []string }{Foo: []string{"huu", "hii", "hoo"}}}, }, { desc: "slice named type", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "huu,hii,hoo", Kind: reflect.Slice}, }, }, element: &struct{ Foo []NamedType }{}, expected: expected{element: &struct{ Foo []NamedType }{Foo: []NamedType{"huu", "hii", "hoo"}}}, }, { desc: "slice named type int", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "1,2,3", Kind: reflect.Slice}, }, }, element: &struct{ Foo []NamedTypeInt }{}, expected: expected{element: &struct{ Foo []NamedTypeInt }{Foo: []NamedTypeInt{1, 2, 3}}}, }, { desc: "empty slice", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "", Kind: reflect.Slice}, }, }, element: &struct{ Foo []string }{}, expected: expected{element: &struct{ Foo []string }{Foo: nil}}, }, { desc: "slice int", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, }, }, element: &struct{ Foo []int }{}, expected: expected{element: &struct{ Foo []int }{Foo: []int{4, 3, 6}}}, }, { desc: "slice invalid int", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, }, }, element: &struct{ Foo []int }{}, expected: expected{error: true}, }, { desc: "slice int8", node: &Node{ Name: "traefik", Kind: reflect.Slice, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, }, }, element: &struct{ Foo []int8 }{}, expected: expected{element: &struct{ Foo []int8 }{Foo: []int8{4, 3, 6}}}, }, { desc: "slice invalid int8", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, }, }, element: &struct{ Foo []int8 }{}, expected: expected{error: true}, }, { desc: "slice int16", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, }, }, element: &struct{ Foo []int16 }{}, expected: expected{element: &struct{ Foo []int16 }{Foo: []int16{4, 3, 6}}}, }, { desc: "slice invalid int16", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, }, }, element: &struct{ Foo []int16 }{}, expected: expected{error: true}, }, { desc: "slice int32", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, }, }, element: &struct{ Foo []int32 }{}, expected: expected{element: &struct{ Foo []int32 }{Foo: []int32{4, 3, 6}}}, }, { desc: "slice invalid int32", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, }, }, element: &struct{ Foo []int32 }{}, expected: expected{error: true}, }, { desc: "slice int64", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, }, }, element: &struct{ Foo []int64 }{}, expected: expected{element: &struct{ Foo []int64 }{Foo: []int64{4, 3, 6}}}, }, { desc: "slice invalid int64", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, }, }, element: &struct{ Foo []int64 }{}, expected: expected{error: true}, }, { desc: "slice uint", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, }, }, element: &struct{ Foo []uint }{}, expected: expected{element: &struct{ Foo []uint }{Foo: []uint{4, 3, 6}}}, }, { desc: "slice invalid uint", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, }, }, element: &struct{ Foo []uint }{}, expected: expected{error: true}, }, { desc: "slice uint8", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, }, }, element: &struct{ Foo []uint8 }{}, expected: expected{element: &struct{ Foo []uint8 }{Foo: []uint8{4, 3, 6}}}, }, { desc: "slice invalid uint8", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, }, }, element: &struct{ Foo []uint8 }{}, expected: expected{error: true}, }, { desc: "slice uint16", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, }, }, element: &struct{ Foo []uint16 }{}, expected: expected{element: &struct{ Foo []uint16 }{Foo: []uint16{4, 3, 6}}}, }, { desc: "slice invalid uint16", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, }, }, element: &struct{ Foo []uint16 }{}, expected: expected{error: true}, }, { desc: "slice uint32", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, }, }, element: &struct{ Foo []uint32 }{}, expected: expected{element: &struct{ Foo []uint32 }{Foo: []uint32{4, 3, 6}}}, }, { desc: "slice invalid uint32", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, }, }, element: &struct{ Foo []uint32 }{}, expected: expected{error: true}, }, { desc: "slice uint64", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, }, }, element: &struct{ Foo []uint64 }{}, expected: expected{element: &struct{ Foo []uint64 }{Foo: []uint64{4, 3, 6}}}, }, { desc: "slice invalid uint64", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, }, }, element: &struct{ Foo []uint64 }{}, expected: expected{error: true}, }, { desc: "slice float32", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, }, }, element: &struct{ Foo []float32 }{}, expected: expected{element: &struct{ Foo []float32 }{Foo: []float32{4, 3, 6}}}, }, { desc: "slice invalid float32", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, }, }, element: &struct{ Foo []float32 }{}, expected: expected{error: true}, }, { desc: "slice float64", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, }, }, element: &struct{ Foo []float64 }{}, expected: expected{element: &struct{ Foo []float64 }{Foo: []float64{4, 3, 6}}}, }, { desc: "slice invalid float64", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, }, }, element: &struct{ Foo []float64 }{}, expected: expected{error: true}, }, { desc: "slice bool", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "true, false, true", Kind: reflect.Slice}, }, }, element: &struct{ Foo []bool }{}, expected: expected{element: &struct{ Foo []bool }{Foo: []bool{true, false, true}}}, }, { desc: "slice invalid bool", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "bool, false, true", Kind: reflect.Slice}, }, }, element: &struct{ Foo []bool }{}, expected: expected{error: true}, }, { desc: "slice slice-as-struct", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Fii", FieldName: "Foo", Kind: reflect.Slice, Tag: `label-slice-as-struct:"Fii"`, Children: []*Node{ {Name: "bar", FieldName: "Bar", Kind: reflect.String, Value: "haa"}, {Name: "bir", FieldName: "Bir", Kind: reflect.String, Value: "hii"}, }, }, }, }, element: &struct { Foo []struct { Bar string Bir string } `label-slice-as-struct:"Fii"` }{}, expected: expected{element: &struct { Foo []struct { Bar string Bir string } `label-slice-as-struct:"Fii"` }{ Foo: []struct { Bar string Bir string }{ { Bar: "haa", Bir: "hii", }, }, }}, }, { desc: "slice slice-as-struct pointer", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Fii", FieldName: "Foo", Kind: reflect.Slice, Tag: `label-slice-as-struct:"Fii"`, Children: []*Node{ {Name: "bar", FieldName: "Bar", Kind: reflect.String, Value: "haa"}, {Name: "bir", FieldName: "Bir", Kind: reflect.String, Value: "hii"}, }, }, }, }, element: &struct { Foo []*struct { Bar string Bir string } `label-slice-as-struct:"Fii"` }{}, expected: expected{element: &struct { Foo []*struct { Bar string Bir string } `label-slice-as-struct:"Fii"` }{ Foo: []*struct { Bar string Bir string }{ { Bar: "haa", Bir: "hii", }, }, }}, }, { desc: "slice slice-as-struct without children", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Fii", FieldName: "Foo", Tag: `label-slice-as-struct:"Fii"`, Kind: reflect.Slice, }, }, }, element: &struct { Foo []struct { Bar string Bir string } `label-slice-as-struct:"Fii"` }{}, expected: expected{error: true}, }, { desc: "pointer SetDefaults method", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Foo", FieldName: "Foo", Kind: reflect.Struct, Children: []*Node{ {Name: "Fuu", FieldName: "Fuu", Value: "huu", Kind: reflect.String}, }, }, }, }, element: &struct { Foo *InitializedFoo }{}, expected: expected{element: &struct { Foo *InitializedFoo }{ Foo: &InitializedFoo{ Fii: "default", Fuu: "huu", }, }}, }, { desc: "pointer wrong SetDefaults method", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Foo", FieldName: "Foo", Kind: reflect.Struct, Children: []*Node{ {Name: "Fuu", FieldName: "Fuu", Value: "huu", Kind: reflect.String}, }, }, }, }, element: &struct { Foo *wrongInitialledFoo }{}, expected: expected{element: &struct { Foo *wrongInitialledFoo }{ Foo: &wrongInitialledFoo{ Fuu: "huu", }, }}, }, { desc: "int pointer", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Pointer}, }, }, element: &struct{ Foo *int }{}, expected: expected{element: &struct{ Foo *int }{Foo: func(v int) *int { return &v }(4)}}, }, { desc: "bool pointer", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "true", Kind: reflect.Pointer}, }, }, element: &struct{ Foo *bool }{}, expected: expected{element: &struct{ Foo *bool }{Foo: func(v bool) *bool { return &v }(true)}}, }, { desc: "string pointer", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "bar", Kind: reflect.Pointer}, }, }, element: &struct{ Foo *string }{}, expected: expected{element: &struct{ Foo *string }{Foo: func(v string) *string { return &v }("bar")}}, }, { desc: "embedded", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Foo", FieldName: "Foo", Kind: reflect.Struct, Children: []*Node{ {Name: "Fuu", FieldName: "Fuu", Value: "huu", Kind: reflect.String}, }, }, }, }, element: &struct { Foo struct { FiiFoo } }{}, expected: expected{element: &struct { Foo struct { FiiFoo } }{ Foo: struct { FiiFoo }{ FiiFoo: FiiFoo{ Fii: "", Fuu: "huu", }, }, }}, }, { desc: "slice struct", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Kind: reflect.Slice, Children: []*Node{ {Name: "[0]", Kind: reflect.Struct, Children: []*Node{ {Name: "Field1", FieldName: "Field1", Value: "A", Kind: reflect.String}, {Name: "Field2", FieldName: "Field2", Value: "A", Kind: reflect.String}, }}, {Name: "[1]", Kind: reflect.Struct, Children: []*Node{ {Name: "Field1", FieldName: "Field1", Value: "B", Kind: reflect.String}, {Name: "Field2", FieldName: "Field2", Value: "B", Kind: reflect.String}, }}, {Name: "[2]", Kind: reflect.Struct, Children: []*Node{ {Name: "Field1", FieldName: "Field1", Value: "C", Kind: reflect.String}, {Name: "Field2", FieldName: "Field2", Value: "C", Kind: reflect.String}, }}, }}, }, }, element: &struct { Foo []struct { Field1 string Field2 string } }{}, expected: expected{element: &struct { Foo []struct { Field1 string Field2 string } }{ Foo: []struct { Field1 string Field2 string }{ {Field1: "A", Field2: "A"}, {Field1: "B", Field2: "B"}, {Field1: "C", Field2: "C"}, }, }}, }, { desc: "slice pointer struct", node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Kind: reflect.Slice, Children: []*Node{ {Name: "[0]", Kind: reflect.Pointer, Children: []*Node{ {Name: "Field1", FieldName: "Field1", Value: "A", Kind: reflect.String}, {Name: "Field2", FieldName: "Field2", Value: "A", Kind: reflect.String}, }}, {Name: "[1]", Kind: reflect.Pointer, Children: []*Node{ {Name: "Field1", FieldName: "Field1", Value: "B", Kind: reflect.String}, {Name: "Field2", FieldName: "Field2", Value: "B", Kind: reflect.String}, }}, {Name: "[2]", Kind: reflect.Pointer, Children: []*Node{ {Name: "Field1", FieldName: "Field1", Value: "C", Kind: reflect.String}, {Name: "Field2", FieldName: "Field2", Value: "C", Kind: reflect.String}, }}, }}, }, }, element: &struct { Foo []*struct { Field1 string Field2 string } }{}, expected: expected{element: &struct { Foo []*struct { Field1 string Field2 string } }{ Foo: []*struct { Field1 string Field2 string }{ {Field1: "A", Field2: "A"}, {Field1: "B", Field2: "B"}, {Field1: "C", Field2: "C"}, }, }}, }, { desc: "raw value", node: &Node{ Name: "traefik", Kind: reflect.Pointer, Children: []*Node{ {Name: "meta", FieldName: "Meta", Kind: reflect.Map, RawValue: map[string]interface{}{ "aaa": "test", "bbb": map[string]interface{}{ "ccc": "test", "ddd": map[string]interface{}{ "eee": "test", }, }, }}, {Name: "name", FieldName: "Name", Value: "test", Kind: reflect.String}, }, }, element: &struct { Name string Meta map[string]interface{} }{}, expected: expected{element: &struct { Name string Meta map[string]interface{} }{ Name: "test", Meta: map[string]interface{}{ "aaa": "test", "bbb": map[string]interface{}{ "ccc": "test", "ddd": map[string]interface{}{ "eee": "test", }, }, }, }}, }, { desc: "explicit map of map, raw value", node: &Node{ Name: "traefik", Kind: reflect.Pointer, Children: []*Node{ {Name: "meta", FieldName: "Meta", Kind: reflect.Map, Children: []*Node{ {Name: "aaa", Kind: reflect.Map, Children: []*Node{ {Name: "bbb", RawValue: map[string]interface{}{ "ccc": "test1", "ddd": "test2", }}, {Name: "eee", Value: "test3", RawValue: map[string]interface{}{ "eee": "test3", }}, }}, }}, {Name: "name", FieldName: "Name", Value: "test", Kind: reflect.String}, }, }, element: &struct { Name string Meta map[string]map[string]interface{} }{}, expected: expected{element: &struct { Name string Meta map[string]map[string]interface{} }{ Name: "test", Meta: map[string]map[string]interface{}{ "aaa": { "bbb": map[string]interface{}{ "ccc": "test1", "ddd": "test2", }, "eee": "test3", }, }, }}, }, { desc: "raw value of slice of map", rawSliceSeparator: ".", node: &Node{ Name: "traefik", Kind: reflect.Pointer, Children: []*Node{{ Name: "meta", FieldName: "Meta", RawValue: map[string]interface{}{ "bar": []interface{}{ map[string]interface{}{ "name": "a", "value": "1", }, map[string]interface{}{ "name": "b", "value": "2", }, }, }, Disabled: false, Kind: reflect.Map, }}, }, element: &struct { Meta map[string]interface{} }{}, expected: expected{element: &struct { Meta map[string]interface{} }{ Meta: map[string]interface{}{ "bar": []interface{}{ map[string]interface{}{"name": "a", "value": "1"}, map[string]interface{}{"name": "b", "value": "2"}, }, }, }}, }, { desc: "recursive slices", rawSliceSeparator: "â•‘", node: &Node{ Name: "traefik", Kind: reflect.Pointer, Children: []*Node{ { Name: "bar", RawValue: map[string]interface{}{ "baz": map[string]interface{}{ "boz": map[string]interface{}{ "foo": "â•‘2â•‘42â•‘42", }, "foo": "â•‘24â•‘fooâ•‘bar", }, "foo": "â•‘24â•‘fooâ•‘bar", }, }, { Name: "foo", Value: "â•‘24â•‘fooâ•‘bar", RawValue: map[string]interface{}{ "foo": "â•‘24â•‘fooâ•‘bar", }, }, }, }, element: &map[string]interface{}{}, expected: expected{element: &map[string]interface{}{ "foo": []interface{}{"foo", "bar"}, "bar": map[string]interface{}{ "foo": []interface{}{"foo", "bar"}, "baz": map[string]interface{}{ "foo": []interface{}{"foo", "bar"}, "boz": map[string]interface{}{ "foo": []interface{}{int64(42), int64(42)}, }, }, }, }}, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() err := newFiller(FillerOpts{AllowSliceAsStruct: true, RawSliceSeparator: test.rawSliceSeparator}).Fill(test.element, test.node) if test.expected.error { require.Error(t, err) } else { require.NoError(t, err) assert.Equal(t, test.expected.element, test.element) } }) } } type ( NamedType string NamedTypeInt int ) type InitializedFoo struct { Fii string Fuu string } func (t *InitializedFoo) SetDefaults() { t.Fii = "default" } type wrongInitialledFoo struct { Fii string Fuu string } func (t *wrongInitialledFoo) SetDefaults() error { t.Fii = "default" return nil } type Bouya string type FiiFoo struct { Fii string Fuu Bouya } paerser-0.2.0/parser/element_nodes.go000066400000000000000000000125211436254333400176260ustar00rootroot00000000000000package parser import ( "fmt" "reflect" "strconv" "strings" ) // EncoderToNodeOpts Options for the encoderToNode. type EncoderToNodeOpts struct { TagName string OmitEmpty bool AllowSliceAsStruct bool } // EncodeToNode converts an element to a node. // element -> nodes. func EncodeToNode(element interface{}, rootName string, opts EncoderToNodeOpts) (*Node, error) { rValue := reflect.ValueOf(element) node := &Node{Name: rootName} encoder := encoderToNode{EncoderToNodeOpts: opts} err := encoder.setNodeValue(node, rValue) if err != nil { return nil, err } return node, nil } type encoderToNode struct { EncoderToNodeOpts } func (e encoderToNode) setNodeValue(node *Node, rValue reflect.Value) error { switch rValue.Kind() { case reflect.String: node.Value = rValue.String() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: node.Value = strconv.FormatInt(rValue.Int(), 10) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: node.Value = strconv.FormatUint(rValue.Uint(), 10) case reflect.Float32, reflect.Float64: node.Value = strconv.FormatFloat(rValue.Float(), 'f', 6, 64) case reflect.Bool: node.Value = strconv.FormatBool(rValue.Bool()) case reflect.Struct: return e.setStructValue(node, rValue) case reflect.Pointer: return e.setNodeValue(node, rValue.Elem()) case reflect.Map: return e.setMapValue(node, rValue) case reflect.Slice: return e.setSliceValue(node, rValue) default: // noop } return nil } func (e encoderToNode) setStructValue(node *Node, rValue reflect.Value) error { rType := rValue.Type() for i := 0; i < rValue.NumField(); i++ { field := rType.Field(i) fieldValue := rValue.Field(i) if !IsExported(field) { continue } if field.Tag.Get(e.TagName) == "-" { continue } if err := isSupportedType(field); err != nil { return err } if e.isSkippedField(field, fieldValue) { continue } nodeName := field.Name if e.AllowSliceAsStruct && field.Type.Kind() == reflect.Slice && len(field.Tag.Get(TagLabelSliceAsStruct)) != 0 { nodeName = field.Tag.Get(TagLabelSliceAsStruct) } if field.Anonymous { if err := e.setNodeValue(node, fieldValue); err != nil { return err } continue } child := &Node{Name: nodeName, FieldName: field.Name, Description: field.Tag.Get(TagDescription)} if err := e.setNodeValue(child, fieldValue); err != nil { return err } if field.Type.Kind() == reflect.Pointer { if field.Type.Elem().Kind() != reflect.Struct && fieldValue.IsNil() { continue } if field.Type.Elem().Kind() == reflect.Struct && len(child.Children) == 0 { if field.Tag.Get(e.TagName) != TagLabelAllowEmpty { continue } child.Value = "true" } } node.Children = append(node.Children, child) } return nil } func (e encoderToNode) setMapValue(node *Node, rValue reflect.Value) error { if rValue.Type().Elem().Kind() == reflect.Interface { node.RawValue = rValue.Interface() return nil } for _, key := range rValue.MapKeys() { child := &Node{Name: key.String(), FieldName: key.String()} node.Children = append(node.Children, child) if err := e.setNodeValue(child, rValue.MapIndex(key)); err != nil { return err } } return nil } func (e encoderToNode) setSliceValue(node *Node, rValue reflect.Value) error { // label-slice-as-struct if rValue.Type().Elem().Kind() == reflect.Struct && !strings.EqualFold(node.Name, node.FieldName) { if rValue.Len() > 1 { return fmt.Errorf("node %s has too many slice entries: %d", node.Name, rValue.Len()) } return e.setNodeValue(node, rValue.Index(0)) } if rValue.Type().Elem().Kind() == reflect.Struct || rValue.Type().Elem().Kind() == reflect.Pointer && rValue.Type().Elem().Elem().Kind() == reflect.Struct { for i := 0; i < rValue.Len(); i++ { child := &Node{Name: "[" + strconv.Itoa(i) + "]"} eValue := rValue.Index(i) err := e.setNodeValue(child, eValue) if err != nil { return err } node.Children = append(node.Children, child) } return nil } var values []string for i := 0; i < rValue.Len(); i++ { eValue := rValue.Index(i) switch eValue.Kind() { case reflect.String: values = append(values, eValue.String()) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: values = append(values, strconv.FormatInt(eValue.Int(), 10)) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: values = append(values, strconv.FormatUint(eValue.Uint(), 10)) case reflect.Float32, reflect.Float64: values = append(values, strconv.FormatFloat(eValue.Float(), 'f', 6, 64)) case reflect.Bool: values = append(values, strconv.FormatBool(eValue.Bool())) default: // noop } } node.Value = strings.Join(values, ", ") return nil } func (e encoderToNode) isSkippedField(field reflect.StructField, fieldValue reflect.Value) bool { if e.OmitEmpty && field.Type.Kind() == reflect.String && fieldValue.Len() == 0 { return true } if field.Type.Kind() == reflect.Pointer && field.Type.Elem().Kind() == reflect.Struct && fieldValue.IsNil() { return true } if e.OmitEmpty && (field.Type.Kind() == reflect.Slice) && (fieldValue.IsNil() || fieldValue.Len() == 0) { return true } if (field.Type.Kind() == reflect.Map) && (fieldValue.IsNil() || fieldValue.Len() == 0) { return true } return false } paerser-0.2.0/parser/element_nodes_test.go000066400000000000000000000413061436254333400206700ustar00rootroot00000000000000package parser import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestEncodeToNode(t *testing.T) { type expected struct { node *Node error bool } testCases := []struct { desc string element interface{} expected expected }{ { desc: "Description", element: struct { Foo string `description:"text"` }{Foo: "bar"}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "bar", Description: "text"}, }}, }, }, { desc: "string", element: struct { Foo string }{Foo: "bar"}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "bar"}, }}, }, }, { desc: "2 string fields", element: struct { Foo string Fii string }{Foo: "bar", Fii: "hii"}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "bar"}, {Name: "Fii", FieldName: "Fii", Value: "hii"}, }}, }, }, { desc: "int", element: struct { Foo int }{Foo: 1}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "1"}, }}, }, }, { desc: "int8", element: struct { Foo int8 }{Foo: 2}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "2"}, }}, }, }, { desc: "int16", element: struct { Foo int16 }{Foo: 2}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "2"}, }}, }, }, { desc: "int32", element: struct { Foo int32 }{Foo: 2}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "2"}, }}, }, }, { desc: "int64", element: struct { Foo int64 }{Foo: 2}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "2"}, }}, }, }, { desc: "uint", element: struct { Foo uint }{Foo: 1}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "1"}, }}, }, }, { desc: "uint8", element: struct { Foo uint8 }{Foo: 2}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "2"}, }}, }, }, { desc: "uint16", element: struct { Foo uint16 }{Foo: 2}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "2"}, }}, }, }, { desc: "uint32", element: struct { Foo uint32 }{Foo: 2}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "2"}, }}, }, }, { desc: "uint64", element: struct { Foo uint64 }{Foo: 2}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "2"}, }}, }, }, { desc: "float32", element: struct { Foo float32 }{Foo: 1.12}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "1.120000"}, }}, }, }, { desc: "float64", element: struct { Foo float64 }{Foo: 1.12}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "1.120000"}, }}, }, }, { desc: "bool", element: struct { Foo bool }{Foo: true}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "true"}, }}, }, }, { desc: "struct", element: struct { Foo struct { Fii string Fuu string } }{ Foo: struct { Fii string Fuu string }{ Fii: "hii", Fuu: "huu", }, }, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Children: []*Node{ {Name: "Fii", FieldName: "Fii", Value: "hii"}, {Name: "Fuu", FieldName: "Fuu", Value: "huu"}, }}, }}, }, }, { desc: "struct unexported field", element: struct { Foo struct { Fii string fuu string } }{ Foo: struct { Fii string fuu string }{ Fii: "hii", fuu: "huu", }, }, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Children: []*Node{ {Name: "Fii", FieldName: "Fii", Value: "hii"}, }}, }}, }, }, { desc: "struct pointer", element: struct { Foo *struct { Fii string Fuu string } }{ Foo: &struct { Fii string Fuu string }{ Fii: "hii", Fuu: "huu", }, }, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Children: []*Node{ {Name: "Fii", FieldName: "Fii", Value: "hii"}, {Name: "Fuu", FieldName: "Fuu", Value: "huu"}, }}, }}, }, }, { desc: "string pointer", element: struct { Foo *struct { Fii *string Fuu string } }{ Foo: &struct { Fii *string Fuu string }{ Fii: func(v string) *string { return &v }("hii"), Fuu: "huu", }, }, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Children: []*Node{ {Name: "Fii", FieldName: "Fii", Value: "hii"}, {Name: "Fuu", FieldName: "Fuu", Value: "huu"}, }}, }}, }, }, { desc: "string nil pointer", element: struct { Foo *struct { Fii *string Fuu string } }{ Foo: &struct { Fii *string Fuu string }{ Fii: nil, Fuu: "huu", }, }, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Children: []*Node{ {Name: "Fuu", FieldName: "Fuu", Value: "huu"}, }}, }}, }, }, { desc: "int pointer", element: struct { Foo *struct { Fii *int Fuu int } }{ Foo: &struct { Fii *int Fuu int }{ Fii: func(v int) *int { return &v }(6), Fuu: 4, }, }, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Children: []*Node{ {Name: "Fii", FieldName: "Fii", Value: "6"}, {Name: "Fuu", FieldName: "Fuu", Value: "4"}, }}, }}, }, }, { desc: "bool pointer", element: struct { Foo *struct { Fii *bool Fuu bool } }{ Foo: &struct { Fii *bool Fuu bool }{ Fii: func(v bool) *bool { return &v }(true), Fuu: true, }, }, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Children: []*Node{ {Name: "Fii", FieldName: "Fii", Value: "true"}, {Name: "Fuu", FieldName: "Fuu", Value: "true"}, }}, }}, }, }, { desc: "struct nil struct pointer", element: struct { Foo *struct { Fii *string Fuu string } }{ Foo: nil, }, expected: expected{node: &Node{Name: "traefik"}}, }, { desc: "struct pointer, not allowEmpty", element: struct { Foo *struct { Fii string Fuu string } }{ Foo: &struct { Fii string Fuu string }{}, }, expected: expected{node: &Node{Name: "traefik"}}, }, { desc: "struct pointer, allowEmpty", element: struct { Foo *struct { Fii string Fuu string } `label:"allowEmpty"` }{ Foo: &struct { Fii string Fuu string }{}, }, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "true"}, }}, }, }, { desc: "map", element: struct { Foo struct { Bar map[string]string } }{ Foo: struct { Bar map[string]string }{ Bar: map[string]string{ "name1": "huu", }, }, }, expected: expected{node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Children: []*Node{ {Name: "Bar", FieldName: "Bar", Children: []*Node{ {Name: "name1", FieldName: "name1", Value: "huu"}, }}, }}, }}}, }, { desc: "empty map", element: struct { Bar map[string]string }{ Bar: map[string]string{}, }, expected: expected{node: &Node{Name: "traefik"}}, }, { desc: "map nil", element: struct { Bar map[string]string }{ Bar: nil, }, expected: expected{node: &Node{Name: "traefik"}}, }, { desc: "ignore map", element: struct { Bar map[string]string `label:"-"` }{ Bar: map[string]string{"huu": "hii"}, }, expected: expected{node: &Node{Name: "traefik"}}, }, { desc: "map with non string key", element: struct { Foo struct { Bar map[int]string } }{ Foo: struct { Bar map[int]string }{ Bar: map[int]string{ 1: "huu", }, }, }, expected: expected{error: true}, }, { desc: "slice of string", element: struct{ Bar []string }{Bar: []string{"huu", "hii"}}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Bar", FieldName: "Bar", Value: "huu, hii"}, }}, }, }, { desc: "slice of int", element: struct{ Bar []int }{Bar: []int{4, 2, 3}}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, }}, }, }, { desc: "slice of int8", element: struct{ Bar []int8 }{Bar: []int8{4, 2, 3}}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, }}, }, }, { desc: "slice of int16", element: struct{ Bar []int16 }{Bar: []int16{4, 2, 3}}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, }}, }, }, { desc: "slice of int32", element: struct{ Bar []int32 }{Bar: []int32{4, 2, 3}}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, }}, }, }, { desc: "slice of int64", element: struct{ Bar []int64 }{Bar: []int64{4, 2, 3}}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, }}, }, }, { desc: "slice of uint", element: struct{ Bar []uint }{Bar: []uint{4, 2, 3}}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, }}, }, }, { desc: "slice of uint8", element: struct{ Bar []uint8 }{Bar: []uint8{4, 2, 3}}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, }}, }, }, { desc: "slice of uint16", element: struct{ Bar []uint16 }{Bar: []uint16{4, 2, 3}}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, }}, }, }, { desc: "slice of uint32", element: struct{ Bar []uint32 }{Bar: []uint32{4, 2, 3}}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, }}, }, }, { desc: "slice of uint64", element: struct{ Bar []uint64 }{Bar: []uint64{4, 2, 3}}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, }}, }, }, { desc: "slice of float32", element: struct{ Bar []float32 }{Bar: []float32{4.1, 2, 3.2}}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Bar", FieldName: "Bar", Value: "4.100000, 2.000000, 3.200000"}, }}, }, }, { desc: "slice of float64", element: struct{ Bar []float64 }{Bar: []float64{4.1, 2, 3.2}}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Bar", FieldName: "Bar", Value: "4.100000, 2.000000, 3.200000"}, }}, }, }, { desc: "slice of bool", element: struct{ Bar []bool }{Bar: []bool{true, false, true}}, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Bar", FieldName: "Bar", Value: "true, false, true"}, }}, }, }, { desc: "slice label-slice-as-struct", element: &struct { Foo []struct { Bar string Bir string } `label-slice-as-struct:"Fii"` }{ Foo: []struct { Bar string Bir string }{ { Bar: "haa", Bir: "hii", }, }, }, expected: expected{node: &Node{ Name: "traefik", Children: []*Node{{ Name: "Fii", FieldName: "Foo", Children: []*Node{ {Name: "Bar", FieldName: "Bar", Value: "haa"}, {Name: "Bir", FieldName: "Bir", Value: "hii"}, }, }}, }}, }, { desc: "slice label-slice-as-struct several slice entries", element: &struct { Foo []struct { Bar string Bir string } `label-slice-as-struct:"Fii"` }{ Foo: []struct { Bar string Bir string }{ { Bar: "haa", Bir: "hii", }, { Bar: "haa", Bir: "hii", }, }, }, expected: expected{error: true}, }, { desc: "slice of struct", element: struct { Foo []struct { Field string } }{ Foo: []struct { Field string }{ { Field: "bar", }, { Field: "bir", }, }, }, expected: expected{node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Children: []*Node{ {Name: "[0]", Children: []*Node{ {Name: "Field", FieldName: "Field", Value: "bar"}, }}, {Name: "[1]", Children: []*Node{ {Name: "Field", FieldName: "Field", Value: "bir"}, }}, }}, }}}, }, { desc: "slice of pointer of struct", element: struct { Foo []*struct { Field string } }{ Foo: []*struct { Field string }{ {Field: "bar"}, {Field: "bir"}, }, }, expected: expected{node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Children: []*Node{ {Name: "[0]", Children: []*Node{ {Name: "Field", FieldName: "Field", Value: "bar"}, }}, {Name: "[1]", Children: []*Node{ {Name: "Field", FieldName: "Field", Value: "bir"}, }}, }}, }}}, }, { desc: "empty slice", element: struct { Bar []string }{ Bar: []string{}, }, expected: expected{node: &Node{Name: "traefik"}}, }, { desc: "nil slice", element: struct { Bar []string }{ Bar: nil, }, expected: expected{node: &Node{Name: "traefik"}}, }, { desc: "ignore slice", element: struct { Bar []string `label:"-"` }{ Bar: []string{"huu", "hii"}, }, expected: expected{node: &Node{Name: "traefik"}}, }, { desc: "embedded", element: struct { Foo struct{ FiiFoo } }{ Foo: struct{ FiiFoo }{ FiiFoo: FiiFoo{ Fii: "hii", Fuu: "huu", }, }, }, expected: expected{ node: &Node{Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Children: []*Node{ {Name: "Fii", FieldName: "Fii", Value: "hii"}, {Name: "Fuu", FieldName: "Fuu", Value: "huu"}, }}, }}, }, }, { desc: "raw value", element: struct { Foo *struct { Bar map[string]interface{} } }{ Foo: &struct { Bar map[string]interface{} }{ Bar: map[string]interface{}{ "AAA": "valueA", "BBB": map[string]interface{}{ "CCC": map[string]interface{}{ "DDD": "valueD", }, }, }, }, }, expected: expected{node: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Children: []*Node{ {Name: "Bar", FieldName: "Bar", RawValue: map[string]interface{}{ "AAA": "valueA", "BBB": map[string]interface{}{ "CCC": map[string]interface{}{ "DDD": "valueD", }, }, }}, }}, }, }}, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() etnOpts := EncoderToNodeOpts{OmitEmpty: true, TagName: TagLabel, AllowSliceAsStruct: true} node, err := EncodeToNode(test.element, DefaultRootName, etnOpts) if test.expected.error { require.Error(t, err) } else { require.NoError(t, err) assert.Equal(t, test.expected.node, node) } }) } } paerser-0.2.0/parser/flat_encode.go000066400000000000000000000101441436254333400172470ustar00rootroot00000000000000package parser import ( "fmt" "reflect" "sort" "strconv" "strings" "time" "github.com/traefik/paerser/types" ) const defaultPtrValue = "false" // FlatOpts holds options used when encoding to Flat. type FlatOpts struct { Case string // "lower" or "upper", defaults to "lower". Separator string SkipRoot bool TagName string } // Flat is a configuration item representation. type Flat struct { Name string Description string Default string } // EncodeToFlat encodes a node to a Flat representation. // Even though the given node argument should have already been augmented with metadata such as kind, // the element (and its type information) is still needed to treat remaining edge cases. func EncodeToFlat(element interface{}, node *Node, opts FlatOpts) ([]Flat, error) { if element == nil || node == nil { return nil, nil } if node.Kind == 0 { return nil, fmt.Errorf("missing node type: %s", node.Name) } elem := reflect.ValueOf(element) if elem.Kind() == reflect.Struct { return nil, fmt.Errorf("structs are not supported, use pointer instead") } encoder := encoderToFlat{FlatOpts: opts} var entries []Flat if encoder.SkipRoot { for _, child := range node.Children { field := encoder.getField(elem.Elem(), child) entries = append(entries, encoder.createFlat(field, child.Name, child)...) } } else { entries = encoder.createFlat(elem, strings.ToLower(node.Name), node) } sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name }) return entries, nil } type encoderToFlat struct { FlatOpts } func (e encoderToFlat) createFlat(field reflect.Value, name string, node *Node) []Flat { var entries []Flat if node.Kind != reflect.Map && node.Description != "-" { if !(node.Kind == reflect.Pointer && len(node.Children) > 0) || (node.Kind == reflect.Pointer && node.Tag.Get(e.TagName) == TagLabelAllowEmpty) { if node.Name[0] != '[' { entries = append(entries, Flat{ Name: e.getName(name), Description: node.Description, Default: e.getNodeValue(e.getField(field, node), node), }) } } } for _, child := range node.Children { if node.Kind == reflect.Map { fChild := e.getField(field, child) var v string if child.Kind == reflect.Struct { v = defaultPtrValue } else { v = e.getNodeValue(fChild, child) } if node.Description != "-" { entries = append(entries, Flat{ Name: e.getName(name, child.Name), Description: node.Description, Default: v, }) } if child.Kind == reflect.Struct || child.Kind == reflect.Pointer { for _, ch := range child.Children { f := e.getField(fChild, ch) n := e.getName(name, child.Name, ch.Name) entries = append(entries, e.createFlat(f, n, ch)...) } } } else { f := e.getField(field, child) n := e.getName(name, child.Name) entries = append(entries, e.createFlat(f, n, child)...) } } return entries } func (e encoderToFlat) getField(field reflect.Value, node *Node) reflect.Value { switch field.Kind() { case reflect.Struct: return field.FieldByName(node.FieldName) case reflect.Pointer: if field.Elem().Kind() == reflect.Struct { return field.Elem().FieldByName(node.FieldName) } return field.Elem() case reflect.Map: return field.MapIndex(reflect.ValueOf(node.FieldName)) default: return field } } func (e encoderToFlat) getNodeValue(field reflect.Value, node *Node) string { if node.Kind == reflect.Pointer && len(node.Children) > 0 { return defaultPtrValue } if field.Kind() == reflect.Int64 { i, _ := strconv.ParseInt(node.Value, 10, 64) switch field.Type() { case reflect.TypeOf(types.Duration(time.Second)): return strconv.Itoa(int(i) / int(time.Second)) case reflect.TypeOf(time.Second): return time.Duration(i).String() } } return node.Value } func (e encoderToFlat) getName(names ...string) string { var name string if names[len(names)-1][0] == '[' { name = strings.Join(names, "") } else { name = strings.Join(names, e.Separator) } if strings.EqualFold(e.Case, "upper") { return strings.ToUpper(name) } return strings.ToLower(name) } paerser-0.2.0/parser/flat_encode_test.go000066400000000000000000000712311436254333400203120ustar00rootroot00000000000000package parser import ( "reflect" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/traefik/paerser/types" ) func TestEncodeToFlat(t *testing.T) { testCases := []struct { desc string element interface{} node *Node opts *FlatOpts expected []Flat }{ { desc: "string field", element: &struct { Field string `description:"field description"` }{ Field: "test", }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Field", FieldName: "Field", Description: "field description", Value: "test", Kind: reflect.String, Tag: `description:"field description"`, }, }, }, expected: []Flat{{ Name: "field", Description: "field description", Default: "test", }}, }, { desc: "int field", element: &struct { Field int `description:"field description"` }{ Field: 6, }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Value: "6", Kind: reflect.Int, Tag: `description:"field description"`, }, }, }, expected: []Flat{{ Name: "field", Description: "field description", Default: "6", }}, }, { desc: "bool field", element: &struct { Field bool `description:"field description"` }{ Field: true, }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Value: "true", Kind: reflect.Bool, Tag: `description:"field description"`, }, }, }, expected: []Flat{{ Name: "field", Description: "field description", Default: "true", }}, }, { desc: "string pointer field", element: &struct { Field *string `description:"field description"` }{ Field: func(v string) *string { return &v }("test"), }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Value: "test", Kind: reflect.Pointer, Tag: `description:"field description"`, }, }, }, expected: []Flat{{ Name: "field", Description: "field description", Default: "test", }}, }, { desc: "string pointer field, custom option", element: &struct { Field *string `description:"field description"` }{ Field: func(v string) *string { return &v }("test"), }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Value: "test", Kind: reflect.Pointer, Tag: `description:"field description"`, }, }, }, opts: &FlatOpts{ Case: "upper", Separator: "_", SkipRoot: false, TagName: TagLabel, }, expected: []Flat{{ Name: "TRAEFIK_FIELD", Description: "field description", Default: "test", }}, }, { desc: "int pointer field", element: &struct { Field *int `description:"field description"` }{ Field: func(v int) *int { return &v }(6), }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Value: "6", Kind: reflect.Pointer, Tag: `description:"field description"`, }, }, }, expected: []Flat{{ Name: "field", Description: "field description", Default: "6", }}, }, { desc: "bool pointer field", element: &struct { Field *bool `description:"field description"` }{ Field: func(v bool) *bool { return &v }(true), }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Value: "true", Kind: reflect.Pointer, Tag: `description:"field description"`, }, }, }, expected: []Flat{{ Name: "field", Description: "field description", Default: "true", }}, }, { desc: "slice of string field, no initial value", element: &struct { Field []string `description:"field description"` }{}, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Kind: reflect.Slice, Tag: `description:"field description"`, }, }, }, expected: []Flat{{ Name: "field", Description: "field description", Default: "", }}, }, { desc: "slice of string field, with initial value", element: &struct { Field []string `description:"field description"` }{ Field: []string{"foo", "bar"}, }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Value: "foo, bar", Kind: reflect.Slice, Tag: `description:"field description"`, }, }, }, expected: []Flat{{ Name: "field", Description: "field description", Default: "foo, bar", }}, }, { desc: "slice of int field, no initial value", element: &struct { Field []int `description:"field description"` }{}, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Kind: reflect.Slice, Tag: `description:"field description"`, }, }, }, expected: []Flat{{ Name: "field", Description: "field description", Default: "", }}, }, { desc: "slice of int field, with initial value", element: &struct { Field []int `description:"field description"` }{ Field: []int{6, 3}, }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Value: "6, 3", Kind: reflect.Slice, Tag: `description:"field description"`, }, }, }, expected: []Flat{{ Name: "field", Description: "field description", Default: "6, 3", }}, }, { desc: "map string field", element: &struct { Field map[string]string `description:"field description"` }{ Field: map[string]string{ MapNamePlaceholder: "", }, }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Kind: reflect.Map, Tag: `description:"field description"`, Children: []*Node{ { Name: "\u003cname\u003e", FieldName: "\u003cname\u003e", Kind: reflect.String, }, }, }, }, }, expected: []Flat{{ Name: "field.", Description: "field description", Default: "", }}, }, { desc: "struct pointer field", element: &struct { Foo *struct { Field string `description:"field description"` } `description:"foo description"` }{ Foo: &struct { Field string `description:"field description"` }{ Field: "test", }, }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Foo", Description: "foo description", FieldName: "Foo", Kind: reflect.Pointer, Tag: `description:"foo description"`, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Value: "test", Kind: reflect.String, Tag: `description:"field description"`, }, }, }, }, }, expected: []Flat{ { Name: "foo.field", Description: "field description", Default: "test", }, }, }, { desc: "struct pointer field, hide field", element: &struct { Foo *struct { Field string `description:"-"` } `description:"foo description"` }{ Foo: &struct { Field string `description:"-"` }{ Field: "test", }, }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Foo", Description: "foo description", FieldName: "Foo", Kind: reflect.Pointer, Tag: `description:"foo description"`, Children: []*Node{ { Name: "Field", Description: "-", FieldName: "Field", Value: "test", Kind: reflect.String, Tag: `description:"-"`, }, }, }, }, }, expected: nil, }, { desc: "struct pointer field, allow empty", element: &struct { Foo *struct { Field string `description:"field description"` } `description:"foo description" label:"allowEmpty"` }{ Foo: &struct { Field string `description:"field description"` }{ Field: "test", }, }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Foo", Description: "foo description", FieldName: "Foo", Kind: reflect.Pointer, Tag: `description:"foo description" label:"allowEmpty"`, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Value: "test", Kind: reflect.String, Tag: `description:"field description"`, }, }, }, }, }, expected: []Flat{ { Name: "foo", Description: "foo description", Default: "false", }, { Name: "foo.field", Description: "field description", Default: "test", }, }, }, { desc: "struct pointer field level 2", element: &struct { Foo *struct { Fii *struct { Field string `description:"field description"` } `description:"fii description"` } `description:"foo description"` }{ Foo: &struct { Fii *struct { Field string `description:"field description"` } `description:"fii description"` }{ Fii: &struct { Field string `description:"field description"` }{ Field: "test", }, }, }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Foo", Description: "foo description", FieldName: "Foo", Kind: reflect.Pointer, Tag: `description:"foo description"`, Children: []*Node{ { Name: "Fii", Description: "fii description", FieldName: "Fii", Kind: reflect.Pointer, Tag: `description:"fii description"`, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Value: "test", Kind: reflect.String, Tag: `description:"field description"`, }, }, }, }, }, }, }, expected: []Flat{ { Name: "foo.fii.field", Description: "field description", Default: "test", }, }, }, { desc: "struct pointer field level 2, allow empty", element: &struct { Foo *struct { Fii *struct { Field string `description:"field description"` } `description:"fii description" label:"allowEmpty"` } `description:"foo description" label:"allowEmpty"` }{ Foo: &struct { Fii *struct { Field string `description:"field description"` } `description:"fii description" label:"allowEmpty"` }{ Fii: &struct { Field string `description:"field description"` }{ Field: "test", }, }, }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Foo", Description: "foo description", FieldName: "Foo", Kind: reflect.Pointer, Tag: `description:"foo description" label:"allowEmpty"`, Children: []*Node{ { Name: "Fii", Description: "fii description", FieldName: "Fii", Kind: reflect.Pointer, Tag: `description:"fii description" label:"allowEmpty"`, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Value: "test", Kind: reflect.String, Tag: `description:"field description"`, }, }, }, }, }, }, }, expected: []Flat{ { Name: "foo", Description: "foo description", Default: "false", }, { Name: "foo.fii", Description: "fii description", Default: "false", }, { Name: "foo.fii.field", Description: "field description", Default: "test", }, }, }, { desc: "map string field level 2", element: &struct { Foo *struct { Fii map[string]string `description:"fii description"` } `description:"foo description"` }{ Foo: &struct { Fii map[string]string `description:"fii description"` }{ Fii: map[string]string{ MapNamePlaceholder: "", }, }, }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Foo", Description: "foo description", FieldName: "Foo", Kind: reflect.Pointer, Tag: `description:"foo description"`, Children: []*Node{ { Name: "Fii", Description: "fii description", FieldName: "Fii", Kind: reflect.Map, Tag: `description:"fii description"`, Children: []*Node{ { Name: "\u003cname\u003e", FieldName: "\u003cname\u003e", Kind: reflect.String, }, }, }, }, }, }, }, expected: []Flat{ { Name: "foo.fii.", Description: "fii description", Default: "", }, }, }, { desc: "map string pointer field level 2", element: &struct { Foo *struct { Fii map[string]*string `description:"fii description"` } `description:"foo description"` }{ Foo: &struct { Fii map[string]*string `description:"fii description"` }{ Fii: map[string]*string{ MapNamePlaceholder: func(v string) *string { return &v }(""), }, }, }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Foo", Description: "foo description", FieldName: "Foo", Kind: reflect.Pointer, Tag: `description:"foo description"`, Children: []*Node{ { Name: "Fii", Description: "fii description", FieldName: "Fii", Kind: reflect.Map, Tag: `description:"fii description"`, Children: []*Node{ { Name: "\u003cname\u003e", FieldName: "\u003cname\u003e", Kind: reflect.Pointer, }, }, }, }, }, }, }, expected: []Flat{ { Name: "foo.fii.", Description: "fii description", Default: "", }, }, }, { desc: "map struct level 1", element: &struct { Foo map[string]struct { Field string `description:"field description"` Yo int `description:"yo description"` } `description:"foo description"` }{}, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Foo", Description: "foo description", FieldName: "Foo", Kind: reflect.Map, Tag: `description:"foo description"`, Children: []*Node{ { Name: "\u003cname\u003e", FieldName: "\u003cname\u003e", Kind: reflect.Struct, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Kind: reflect.String, Tag: `description:"field description"`, }, { Name: "Yo", Description: "yo description", FieldName: "Yo", Value: "0", Kind: reflect.Int, Tag: `description:"yo description"`, }, }, }, }, }, }, }, expected: []Flat{ { Name: "foo.", Description: "foo description", Default: "false", }, { Name: "foo..field", Description: "field description", Default: "", }, { Name: "foo..yo", Description: "yo description", Default: "0", }, }, }, { desc: "map struct pointer level 1", element: &struct { Foo map[string]*struct { Field string `description:"field description"` Yo string `description:"yo description"` } `description:"foo description"` }{}, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Foo", Description: "foo description", FieldName: "Foo", Kind: reflect.Map, Tag: `description:"foo description"`, Children: []*Node{ { Name: "\u003cname\u003e", FieldName: "\u003cname\u003e", Kind: reflect.Pointer, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Kind: reflect.String, Tag: `description:"field description"`, }, { Name: "Yo", Description: "yo description", FieldName: "Yo", Kind: reflect.String, Tag: `description:"yo description"`, }, }, }, }, }, }, }, expected: []Flat{ { Name: "foo.", Description: "foo description", Default: "false", }, { Name: "foo..field", Description: "field description", Default: "", }, { Name: "foo..yo", Description: "yo description", Default: "", }, }, }, { desc: "time duration field", element: &struct { Field time.Duration `description:"field description"` }{ Field: 1 * time.Second, }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Value: "1000000000", Kind: reflect.Int64, Tag: `description:"field description"`, }, }, }, expected: []Flat{{ Name: "field", Description: "field description", Default: "1s", }}, }, { desc: "time duration field map", element: &struct { Foo map[string]*struct { Field time.Duration `description:"field description"` } `description:"foo description"` }{ Foo: map[string]*struct { Field time.Duration `description:"field description"` }{ "": { Field: 0, }, }, }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Foo", Description: "foo description", FieldName: "Foo", Kind: reflect.Map, Tag: `description:"foo description"`, Children: []*Node{ { Name: "\u003cname\u003e", FieldName: "\u003cname\u003e", Kind: reflect.Pointer, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Value: "0", Kind: reflect.Int64, Tag: `description:"field description"`, }, }, }, }, }, }, }, expected: []Flat{ { Name: "foo.", Description: "foo description", Default: "false", }, { Name: "foo..field", Description: "field description", Default: "0s", }, }, }, { desc: "time duration field map 2", element: &struct { Foo map[string]*struct { Fii *struct { Field time.Duration `description:"field description"` } } `description:"foo description"` }{ Foo: map[string]*struct { Fii *struct { Field time.Duration `description:"field description"` } }{ "": { Fii: &struct { Field time.Duration `description:"field description"` }{ Field: 0, }, }, }, }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Foo", Description: "foo description", FieldName: "Foo", Kind: reflect.Map, Tag: `description:"foo description"`, Children: []*Node{ { Name: "\u003cname\u003e", FieldName: "\u003cname\u003e", Kind: reflect.Pointer, Children: []*Node{ { Name: "Fii", FieldName: "Fii", Kind: reflect.Pointer, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Value: "0", Kind: reflect.Int64, Tag: `description:"field description"`, }, }, }, }, }, }, }, }, }, expected: []Flat{ { Name: "foo.", Description: "foo description", Default: "false", }, { Name: "foo..fii.field", Description: "field description", Default: "0s", }, }, }, { desc: "time duration field 2", element: &struct { Foo *struct { Field time.Duration `description:"field description"` } }{ Foo: &struct { Field time.Duration `description:"field description"` }{ Field: 1 * time.Second, }, }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Foo", FieldName: "Foo", Kind: reflect.Pointer, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Value: "1000000000", Kind: reflect.Int64, Tag: `description:"field description"`, }, }, }, }, }, expected: []Flat{{ Name: "foo.field", Description: "field description", Default: "1s", }}, }, { desc: "time duration field 3", element: &struct { Foo *struct { Fii *struct { Field time.Duration `description:"field description"` } } }{ Foo: &struct { Fii *struct { Field time.Duration `description:"field description"` } }{ Fii: &struct { Field time.Duration `description:"field description"` }{ Field: 1 * time.Second, }, }, }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Pointer, Children: []*Node{ { Name: "Foo", FieldName: "Foo", Kind: reflect.Pointer, Children: []*Node{ { Name: "Fii", FieldName: "Fii", Kind: reflect.Pointer, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Value: "1000000000", Kind: reflect.Int64, Tag: `description:"field description"`, }, }, }, }, }, }, }, expected: []Flat{{ Name: "foo.fii.field", Description: "field description", Default: "1s", }}, }, { desc: "time duration field", element: &struct { Field types.Duration `description:"field description"` }{ Field: types.Duration(180 * time.Second), }, node: &Node{ Name: "traefik", FieldName: "", Kind: reflect.Struct, Children: []*Node{ { Name: "Field", Description: "field description", FieldName: "Field", Value: "180000000000", Kind: reflect.Int64, Tag: `description:"field description"`, }, }, }, expected: []Flat{{ Name: "field", Description: "field description", Default: "180", }}, }, { desc: "slice of struct", element: &struct { Foo *struct { Fii []struct { Field1 string `description:"field1 description"` Field2 int `description:"field2 description"` } `description:"fii description"` } `description:"foo description"` }{ Foo: &struct { Fii []struct { Field1 string `description:"field1 description"` Field2 int `description:"field2 description"` } `description:"fii description"` }{ Fii: []struct { Field1 string `description:"field1 description"` Field2 int `description:"field2 description"` }{ { Field1: "", Field2: 0, }, }, }, }, node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", Kind: reflect.Pointer, Description: "foo description", Children: []*Node{ {Name: "Fii", Kind: reflect.Slice, Description: "fii description", Children: []*Node{ {Name: "[0]", Kind: reflect.Struct, Children: []*Node{ {Name: "Field1", Value: "", Kind: reflect.String, Description: "field1 description"}, {Name: "Field2", Value: "0", Kind: reflect.Int, Description: "field2 description"}, }}, }}, }}, }, }, expected: []Flat{ { Name: "foo.fii", Description: "fii description", Default: "", }, { Name: "foo.fii[0].field1", Description: "field1 description", Default: "", }, { Name: "foo.fii[0].field2", Description: "field2 description", Default: "0", }, }, }, // Skipped: because realistically not needed in Traefik for now. // { // desc: "map of map field level 2", // element: &struct { // Foo *struct { // Fii map[string]map[string]string `description:"fii description"` // } `description:"foo description"` // }{ // Foo: &struct { // Fii map[string]map[string]string `description:"fii description"` // }{ // Fii: map[string]map[string]string{ // MapNamePlaceholder: { // MapNamePlaceholder: "test", // }, // }, // }, // }, // expected: `XXX`, // }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() var opts FlatOpts if test.opts == nil { opts = FlatOpts{Separator: ".", SkipRoot: true, TagName: TagLabel} } else { opts = *test.opts } entries, err := EncodeToFlat(test.element, test.node, opts) require.NoError(t, err) assert.Equal(t, test.expected, entries) }) } } paerser-0.2.0/parser/labels_decode.go000066400000000000000000000041351436254333400175540ustar00rootroot00000000000000package parser import ( "fmt" "sort" "strings" ) // DecodeToNode converts the labels to a tree of nodes. // If any filters are present, labels which do not match the filters are skipped. func DecodeToNode(labels map[string]string, rootName string, filters ...string) (*Node, error) { sortedKeys := sortKeys(labels, filters) var node *Node for i, key := range sortedKeys { split := strings.Split(key, ".") if split[0] != rootName { return nil, fmt.Errorf("invalid label root %s", split[0]) } var parts []string for _, v := range split { if v == "" { return nil, fmt.Errorf("invalid element: %s", key) } if v[0] == '[' { return nil, fmt.Errorf("invalid leading character '[' in field name (bracket is a slice delimiter): %s", v) } if strings.HasSuffix(v, "]") && v[0] != '[' { indexLeft := strings.Index(v, "[") parts = append(parts, v[:indexLeft], v[indexLeft:]) } else { parts = append(parts, v) } } if i == 0 { node = &Node{} } decodeToNode(node, parts, labels[key]) } return node, nil } func decodeToNode(root *Node, path []string, value string) { if len(root.Name) == 0 { root.Name = path[0] } // it's a leaf or not -> children if len(path) > 1 { if n := containsNode(root.Children, path[1]); n != nil { // the child already exists decodeToNode(n, path[1:], value) } else { // new child child := &Node{Name: path[1]} decodeToNode(child, path[1:], value) root.Children = append(root.Children, child) } } else { root.Value = value } } func containsNode(nodes []*Node, name string) *Node { for _, n := range nodes { if strings.EqualFold(name, n.Name) { return n } } return nil } func sortKeys(labels map[string]string, filters []string) []string { var sortedKeys []string for key := range labels { if len(filters) == 0 { sortedKeys = append(sortedKeys, key) continue } for _, filter := range filters { if len(key) >= len(filter) && strings.EqualFold(key[:len(filter)], filter) { sortedKeys = append(sortedKeys, key) continue } } } sort.Strings(sortedKeys) return sortedKeys } paerser-0.2.0/parser/labels_decode_test.go000066400000000000000000000126251436254333400206160ustar00rootroot00000000000000package parser import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDecodeToNode(t *testing.T) { type expected struct { error bool node *Node } testCases := []struct { desc string in map[string]string filters []string expected expected }{ { desc: "no label", in: map[string]string{}, expected: expected{node: nil}, }, { desc: "invalid label, ending by a dot", in: map[string]string{ "traefik.http.": "bar", }, expected: expected{ error: true, }, }, { desc: "level 1", in: map[string]string{ "traefik.foo": "bar", }, expected: expected{node: &Node{ Name: "traefik", Children: []*Node{ {Name: "foo", Value: "bar"}, }, }}, }, { desc: "level 1 empty value", in: map[string]string{ "traefik.foo": "", }, expected: expected{node: &Node{ Name: "traefik", Children: []*Node{ {Name: "foo", Value: ""}, }, }}, }, { desc: "level 2", in: map[string]string{ "traefik.foo.bar": "bar", }, expected: expected{node: &Node{ Name: "traefik", Children: []*Node{{ Name: "foo", Children: []*Node{ {Name: "bar", Value: "bar"}, }, }}, }}, }, { desc: "several entries, level 0", in: map[string]string{ "traefik": "bar", "traefic": "bur", }, expected: expected{error: true}, }, { desc: "several entries, prefix filter", in: map[string]string{ "traefik.foo": "bar", "traefik.fii": "bir", }, filters: []string{"traefik.Foo"}, expected: expected{node: &Node{ Name: "traefik", Children: []*Node{ {Name: "foo", Value: "bar"}, }, }}, }, { desc: "several entries, level 1", in: map[string]string{ "traefik.foo": "bar", "traefik.fii": "bur", }, expected: expected{node: &Node{ Name: "traefik", Children: []*Node{ {Name: "fii", Value: "bur"}, {Name: "foo", Value: "bar"}, }, }}, }, { desc: "several entries, level 2", in: map[string]string{ "traefik.foo.aaa": "bar", "traefik.foo.bbb": "bur", }, expected: expected{node: &Node{ Name: "traefik", Children: []*Node{ {Name: "foo", Children: []*Node{ {Name: "aaa", Value: "bar"}, {Name: "bbb", Value: "bur"}, }}, }, }}, }, { desc: "several entries, level 2, case-insensitive", in: map[string]string{ "traefik.foo.aaa": "bar", "traefik.Foo.bbb": "bur", }, expected: expected{node: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Children: []*Node{ {Name: "bbb", Value: "bur"}, {Name: "aaa", Value: "bar"}, }}, }, }}, }, { desc: "several entries, level 2, 3 children", in: map[string]string{ "traefik.foo.aaa": "bar", "traefik.foo.bbb": "bur", "traefik.foo.ccc": "bir", }, expected: expected{node: &Node{ Name: "traefik", Children: []*Node{ {Name: "foo", Children: []*Node{ {Name: "aaa", Value: "bar"}, {Name: "bbb", Value: "bur"}, {Name: "ccc", Value: "bir"}, }}, }, }}, }, { desc: "several entries, level 3", in: map[string]string{ "traefik.foo.bar.aaa": "bar", "traefik.foo.bar.bbb": "bur", }, expected: expected{node: &Node{ Name: "traefik", Children: []*Node{ {Name: "foo", Children: []*Node{ {Name: "bar", Children: []*Node{ {Name: "aaa", Value: "bar"}, {Name: "bbb", Value: "bur"}, }}, }}, }, }}, }, { desc: "several entries, level 3, 2 children level 1", in: map[string]string{ "traefik.foo.bar.aaa": "bar", "traefik.foo.bar.bbb": "bur", "traefik.bar.foo.bbb": "bir", }, expected: expected{node: &Node{ Name: "traefik", Children: []*Node{ {Name: "bar", Children: []*Node{ {Name: "foo", Children: []*Node{ {Name: "bbb", Value: "bir"}, }}, }}, {Name: "foo", Children: []*Node{ {Name: "bar", Children: []*Node{ {Name: "aaa", Value: "bar"}, {Name: "bbb", Value: "bur"}, }}, }}, }, }}, }, { desc: "several entries, slice syntax", in: map[string]string{ "traefik.foo[0].aaa": "bar0", "traefik.foo[0].bbb": "bur0", "traefik.foo[1].aaa": "bar1", "traefik.foo[1].bbb": "bur1", }, expected: expected{node: &Node{ Name: "traefik", Children: []*Node{ {Name: "foo", Children: []*Node{ {Name: "[0]", Children: []*Node{ {Name: "aaa", Value: "bar0"}, {Name: "bbb", Value: "bur0"}, }}, {Name: "[1]", Children: []*Node{ {Name: "aaa", Value: "bar1"}, {Name: "bbb", Value: "bur1"}, }}, }}, }, }}, }, { desc: "several entries, invalid slice syntax", in: map[string]string{ "traefik.foo.[0].aaa": "bar0", "traefik.foo.[0].bbb": "bur0", "traefik.foo.[1].aaa": "bar1", "traefik.foo.[1].bbb": "bur1", }, expected: expected{error: true}, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() out, err := DecodeToNode(test.in, DefaultRootName, test.filters...) if test.expected.error { require.Error(t, err) } else { require.NoError(t, err) if !assert.Equal(t, test.expected.node, out) { bytes, err := json.MarshalIndent(out, "", " ") require.NoError(t, err) t.Log(string(bytes)) } } }) } } paerser-0.2.0/parser/labels_encode.go000066400000000000000000000025711436254333400175700ustar00rootroot00000000000000package parser import ( "fmt" "reflect" ) // EncodeNode Converts a node to labels. // nodes -> labels. func EncodeNode(node *Node) map[string]string { labels := make(map[string]string) encodeNode(labels, node.Name, node) return labels } func encodeNode(labels map[string]string, root string, node *Node) { for _, child := range node.Children { if child.Disabled { continue } var sep string if child.Name[0] != '[' { sep = "." } childName := root + sep + child.Name if child.RawValue != nil { encodeRawValue(labels, childName, child.RawValue) continue } if len(child.Children) > 0 { encodeNode(labels, childName, child) } else if len(child.Name) > 0 { labels[childName] = child.Value } } } func encodeRawValue(labels map[string]string, root string, rawValue interface{}) { if rawValue == nil { return } tValue := reflect.TypeOf(rawValue) if tValue.Kind() == reflect.Map && tValue.Elem().Kind() == reflect.Interface { r := reflect.ValueOf(rawValue). Convert(reflect.TypeOf((map[string]interface{})(nil))). Interface().(map[string]interface{}) for k, v := range r { switch tv := v.(type) { case string: labels[root+"."+k] = tv case []interface{}: for i, e := range tv { encodeRawValue(labels, fmt.Sprintf("%s.%s[%d]", root, k, i), e) } default: encodeRawValue(labels, root+"."+k, v) } } } } paerser-0.2.0/parser/labels_encode_test.go000066400000000000000000000112111436254333400206160ustar00rootroot00000000000000package parser import ( "testing" "github.com/stretchr/testify/assert" ) func TestEncodeNode(t *testing.T) { testCases := []struct { desc string node *Node expected map[string]string }{ { desc: "1 label", node: &Node{ Name: "traefik", Children: []*Node{ {Name: "aaa", Value: "bar"}, }, }, expected: map[string]string{ "traefik.aaa": "bar", }, }, { desc: "2 labels", node: &Node{ Name: "traefik", Children: []*Node{ {Name: "aaa", Value: "bar"}, {Name: "bbb", Value: "bur"}, }, }, expected: map[string]string{ "traefik.aaa": "bar", "traefik.bbb": "bur", }, }, { desc: "2 labels, 1 disabled", node: &Node{ Name: "traefik", Children: []*Node{ {Name: "aaa", Value: "bar"}, {Name: "bbb", Value: "bur", Disabled: true}, }, }, expected: map[string]string{ "traefik.aaa": "bar", }, }, { desc: "2 levels", node: &Node{ Name: "traefik", Children: []*Node{ {Name: "foo", Children: []*Node{ {Name: "aaa", Value: "bar"}, }}, }, }, expected: map[string]string{ "traefik.foo.aaa": "bar", }, }, { desc: "3 levels", node: &Node{ Name: "traefik", Children: []*Node{ {Name: "foo", Children: []*Node{ {Name: "bar", Children: []*Node{ {Name: "aaa", Value: "bar"}, }}, }}, }, }, expected: map[string]string{ "traefik.foo.bar.aaa": "bar", }, }, { desc: "2 levels, same root", node: &Node{ Name: "traefik", Children: []*Node{ {Name: "foo", Children: []*Node{ {Name: "bar", Children: []*Node{ {Name: "aaa", Value: "bar"}, {Name: "bbb", Value: "bur"}, }}, }}, }, }, expected: map[string]string{ "traefik.foo.bar.aaa": "bar", "traefik.foo.bar.bbb": "bur", }, }, { desc: "several levels, different root", node: &Node{ Name: "traefik", Children: []*Node{ {Name: "bar", Children: []*Node{ {Name: "ccc", Value: "bir"}, }}, {Name: "foo", Children: []*Node{ {Name: "bar", Children: []*Node{ {Name: "aaa", Value: "bar"}, }}, }}, }, }, expected: map[string]string{ "traefik.foo.bar.aaa": "bar", "traefik.bar.ccc": "bir", }, }, { desc: "multiple labels, multiple levels", node: &Node{ Name: "traefik", Children: []*Node{ {Name: "bar", Children: []*Node{ {Name: "ccc", Value: "bir"}, }}, {Name: "foo", Children: []*Node{ {Name: "bar", Children: []*Node{ {Name: "aaa", Value: "bar"}, {Name: "bbb", Value: "bur"}, }}, }}, }, }, expected: map[string]string{ "traefik.foo.bar.aaa": "bar", "traefik.foo.bar.bbb": "bur", "traefik.bar.ccc": "bir", }, }, { desc: "slice of struct syntax", node: &Node{ Name: "traefik", Children: []*Node{ {Name: "foo", Children: []*Node{ {Name: "[0]", Children: []*Node{ {Name: "aaa", Value: "bar0"}, {Name: "bbb", Value: "bur0"}, }}, {Name: "[1]", Children: []*Node{ {Name: "aaa", Value: "bar1"}, {Name: "bbb", Value: "bur1"}, }}, }}, }, }, expected: map[string]string{ "traefik.foo[0].aaa": "bar0", "traefik.foo[0].bbb": "bur0", "traefik.foo[1].aaa": "bar1", "traefik.foo[1].bbb": "bur1", }, }, { desc: "raw value, level 1", node: &Node{ Name: "traefik", Children: []*Node{ {Name: "aaa", RawValue: map[string]interface{}{ "bbb": "test1", "ccc": "test2", }}, }, }, expected: map[string]string{ "traefik.aaa.bbb": "test1", "traefik.aaa.ccc": "test2", }, }, { desc: "raw value, level 2", node: &Node{ Name: "traefik", Children: []*Node{ {Name: "aaa", RawValue: map[string]interface{}{ "bbb": "test1", "ccc": map[string]interface{}{ "ddd": "test2", }, }}, }, }, expected: map[string]string{ "traefik.aaa.bbb": "test1", "traefik.aaa.ccc.ddd": "test2", }, }, { desc: "raw value, slice of struct", node: &Node{ Name: "traefik", Children: []*Node{ {Name: "aaa", RawValue: map[string]interface{}{ "bbb": []interface{}{ map[string]interface{}{ "ccc": "test1", "ddd": "test2", }, }, }}, }, }, expected: map[string]string{ "traefik.aaa.bbb[0].ccc": "test1", "traefik.aaa.bbb[0].ddd": "test2", }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() labels := EncodeNode(test.node) assert.Equal(t, test.expected, labels) }) } } paerser-0.2.0/parser/node.go000066400000000000000000000014611436254333400157330ustar00rootroot00000000000000package parser import "reflect" // DefaultRootName is the default name of the root node and the prefix of element name from the resources. const DefaultRootName = "traefik" // MapNamePlaceholder is the placeholder for the map name. const MapNamePlaceholder = "" // Node is a label node. type Node struct { Name string `json:"name"` Description string `json:"description,omitempty"` FieldName string `json:"fieldName"` Value string `json:"value,omitempty"` RawValue interface{} `json:"rawValue,omitempty"` Disabled bool `json:"disabled,omitempty"` Kind reflect.Kind `json:"kind,omitempty"` Tag reflect.StructTag `json:"tag,omitempty"` Children []*Node `json:"children,omitempty"` } paerser-0.2.0/parser/nodes_metadata.go000066400000000000000000000143041436254333400177560ustar00rootroot00000000000000package parser import ( "errors" "fmt" "reflect" "strings" ) // MetadataOpts Options for the metadata. type MetadataOpts struct { TagName string AllowSliceAsStruct bool } // AddMetadata adds metadata such as type, inferred from element, to a node. func AddMetadata(element interface{}, node *Node, opts MetadataOpts) error { return metadata{MetadataOpts: opts}.Add(element, node) } type metadata struct { MetadataOpts } // Add adds metadata such as type, inferred from element, to a node. func (m metadata) Add(element interface{}, node *Node) error { if node == nil { return nil } if len(node.Children) == 0 { return fmt.Errorf("invalid node %s: no child", node.Name) } if element == nil { return errors.New("nil structure") } rootType := reflect.TypeOf(element) node.Kind = rootType.Kind() return m.browseChildren(rootType, node) } func (m metadata) browseChildren(fType reflect.Type, node *Node) error { for _, child := range node.Children { if err := m.add(fType, child); err != nil { return err } } return nil } func (m metadata) add(rootType reflect.Type, node *Node) error { rType := rootType if rootType.Kind() == reflect.Pointer { rType = rootType.Elem() } if rType.Kind() == reflect.Map && rType.Elem().Kind() == reflect.Interface { addRawValue(node) return nil } field, err := m.findTypedField(rType, node) if err != nil { return err } if err = isSupportedType(field); err != nil { return err } fType := field.Type node.Kind = fType.Kind() node.Tag = field.Tag if fType.Kind() == reflect.Struct || fType.Kind() == reflect.Pointer && fType.Elem().Kind() == reflect.Struct || fType.Kind() == reflect.Map { if len(node.Children) == 0 && !(field.Tag.Get(m.TagName) == TagLabelAllowEmpty || field.Tag.Get(m.TagName) == "-") { return fmt.Errorf("%s cannot be a standalone element (type %s)", node.Name, fType) } node.Disabled = len(node.Value) > 0 && !strings.EqualFold(node.Value, "true") && field.Tag.Get(m.TagName) == TagLabelAllowEmpty } node.Disabled = node.Disabled || field.Tag.Get(m.TagName) == "-" if len(node.Children) == 0 { return nil } if fType.Kind() == reflect.Struct || fType.Kind() == reflect.Pointer && fType.Elem().Kind() == reflect.Struct { return m.browseChildren(fType, node) } if fType.Kind() == reflect.Map { if fType.Elem().Kind() == reflect.Interface { addRawValue(node) return nil } for _, child := range node.Children { // elem is a map entry value type elem := fType.Elem() child.Kind = elem.Kind() if elem.Kind() == reflect.Map || elem.Kind() == reflect.Struct || (elem.Kind() == reflect.Pointer && elem.Elem().Kind() == reflect.Struct) { if err = m.browseChildren(elem, child); err != nil { return err } } } return nil } if fType.Kind() == reflect.Slice { if m.AllowSliceAsStruct && field.Tag.Get(TagLabelSliceAsStruct) != "" { return m.browseChildren(fType.Elem(), node) } for _, ch := range node.Children { ch.Kind = fType.Elem().Kind() if err = m.browseChildren(fType.Elem(), ch); err != nil { return err } } return nil } return fmt.Errorf("invalid node %s: %v", node.Name, fType.Kind()) } func (m metadata) findTypedField(rType reflect.Type, node *Node) (reflect.StructField, error) { if rType.Kind() != reflect.Struct { return reflect.StructField{}, fmt.Errorf("field not found, node: %s", node.Name) } for i := 0; i < rType.NumField(); i++ { cField := rType.Field(i) fieldName := cField.Tag.Get(TagLabelSliceAsStruct) if !m.AllowSliceAsStruct || len(fieldName) == 0 { fieldName = cField.Name } if IsExported(cField) { if cField.Anonymous { if cField.Type.Kind() == reflect.Struct { structField, err := m.findTypedField(cField.Type, node) if err != nil { continue } return structField, nil } } if strings.EqualFold(fieldName, node.Name) { node.FieldName = cField.Name return cField, nil } } } return reflect.StructField{}, fmt.Errorf("field not found, node: %s", node.Name) } // IsExported reports whether f is exported. // https://golang.org/pkg/reflect/#StructField func IsExported(f reflect.StructField) bool { return f.PkgPath == "" } func isSupportedType(field reflect.StructField) error { fType := field.Type if fType.Kind() == reflect.Slice { switch fType.Elem().Kind() { case reflect.String, reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64, reflect.Struct, reflect.Pointer: return nil default: return fmt.Errorf("unsupported slice type: %v", fType) } } if fType.Kind() == reflect.Map && fType.Key().Kind() != reflect.String { return fmt.Errorf("unsupported map key type: %v", fType.Key()) } if fType.Kind() == reflect.Func { return fmt.Errorf("unsupported type: %v", fType) } return nil } /* RawMap section */ func addRawValue(node *Node) { if node.RawValue == nil { node.RawValue = nodeToRawMap(node) } node.Children = nil } func nodeToRawMap(node *Node) map[string]interface{} { result := map[string]interface{}{} squashNode(node, result, true) return result } func squashNode(node *Node, acc map[string]interface{}, root bool) { if len(node.Children) == 0 { acc[node.Name] = node.Value return } // slice if isArrayKey(node.Children[0].Name) { var accChild []interface{} for _, child := range node.Children { tmp := map[string]interface{}{} squashNode(child, tmp, false) accChild = append(accChild, tmp[child.Name]) } acc[node.Name] = accChild return } // map var accChild map[string]interface{} if root { accChild = acc } else { accChild = typedRawMap(acc, node.Name) } for _, child := range node.Children { squashNode(child, accChild, false) } } func typedRawMap(m map[string]interface{}, k string) map[string]interface{} { if m[k] == nil { m[k] = map[string]interface{}{} } r, ok := m[k].(map[string]interface{}) if !ok { panic(fmt.Sprintf("unsupported value (key: %s): %T", k, m[k])) } return r } func isArrayKey(name string) bool { return name[0] == '[' && name[len(name)-1] == ']' } paerser-0.2.0/parser/nodes_metadata_test.go000066400000000000000000000623121436254333400210170ustar00rootroot00000000000000package parser import ( "encoding/json" "reflect" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type MySliceType []string func TestAddMetadata(t *testing.T) { type expected struct { node *Node error bool } type interf interface{} testCases := []struct { desc string tree *Node structure interface{} expected expected }{ { desc: "Node Nil", tree: nil, structure: nil, expected: expected{node: nil}, }, { desc: "Empty Node", tree: &Node{}, structure: nil, expected: expected{error: true}, }, { desc: "Nil structure", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Value: "bar"}, }, }, structure: nil, expected: expected{error: true}, }, { desc: "level 0", tree: &Node{Name: "traefik", Value: "bar"}, expected: expected{error: true}, }, { desc: "level 1", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "bar"}, }, }, structure: struct{ Foo string }{}, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "bar", Kind: reflect.String}, }, }, }, }, { desc: "level 1, pointer", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Value: "bar"}, }, }, structure: &struct{ Foo string }{}, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Pointer, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "bar", Kind: reflect.String}, }, }, }, }, { desc: "level 1, slice", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Value: "bar,bur"}, }, }, structure: struct{ Foo []string }{}, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "bar,bur", Kind: reflect.Slice}, }, }, }, }, { desc: "level 1, interface", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Value: "", Children: []*Node{ {Name: "Fii", Value: "hii"}, }}, }, }, structure: struct{ Foo interf }{}, expected: expected{error: true}, }, { desc: "level 1, map string", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Children: []*Node{ {Name: "name1", Value: "bar"}, {Name: "name2", Value: "bur"}, }}, }, }, structure: struct{ Foo map[string]string }{}, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Kind: reflect.Map, Children: []*Node{ {Name: "name1", Value: "bar", Kind: reflect.String}, {Name: "name2", Value: "bur", Kind: reflect.String}, }}, }, }, }, }, { desc: "level 1, map struct", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Children: []*Node{ {Name: "name1", Children: []*Node{ {Name: "Fii", Value: "bar"}, }}, }}, }, }, structure: struct { Foo map[string]struct{ Fii string } }{}, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Kind: reflect.Map, Children: []*Node{ {Name: "name1", Kind: reflect.Struct, Children: []*Node{ {Name: "Fii", FieldName: "Fii", Value: "bar", Kind: reflect.String}, }}, }}, }, }, }, }, { desc: "level 1, map int as key", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Children: []*Node{ {Name: "name1", Children: []*Node{ {Name: "Fii", Value: "bar"}, }}, }}, }, }, structure: struct { Foo map[int]struct{ Fii string } }{}, expected: expected{error: true}, }, { desc: "level 1, int pointer", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Value: "0"}, }, }, structure: struct { Foo *int }{}, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "0", Kind: reflect.Pointer}, }, }, }, }, { desc: "level 1, bool pointer", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Value: "0"}, }, }, structure: struct { Foo *bool }{}, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "0", Kind: reflect.Pointer}, }, }, }, }, { desc: "level 1, string pointer", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Value: "0"}, }, }, structure: struct { Foo *string }{}, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "0", Kind: reflect.Pointer}, }, }, }, }, { desc: "level 1, 2 children with different types", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Value: "bar"}, {Name: "Fii", Value: "1"}, }, }, structure: struct { Foo string Fii int }{}, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "bar", Kind: reflect.String}, {Name: "Fii", FieldName: "Fii", Value: "1", Kind: reflect.Int}, }, }, }, }, { desc: "level 1, use exported instead of unexported", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "foo", Value: "bar"}, }, }, structure: struct { foo int Foo string }{}, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "foo", Value: "bar", FieldName: "Foo", Kind: reflect.String}, }, }, }, }, { desc: "level 1, unexported", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "foo", Value: "bar"}, }, }, structure: struct { foo string }{}, expected: expected{error: true}, }, { desc: "level 1, 3 children with different types", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Value: "bar"}, {Name: "Fii", Value: "1"}, {Name: "Fuu", Value: "true"}, }, }, structure: struct { Foo string Fii int Fuu bool }{}, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "bar", Kind: reflect.String}, {Name: "Fii", FieldName: "Fii", Value: "1", Kind: reflect.Int}, {Name: "Fuu", FieldName: "Fuu", Value: "true", Kind: reflect.Bool}, }, }, }, }, { desc: "level 2", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Children: []*Node{ {Name: "Bar", Value: "bir"}, }}, }, }, structure: struct { Foo struct { Bar string } }{ Foo: struct { Bar string }{}, }, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Kind: reflect.Struct, Children: []*Node{ {Name: "Bar", FieldName: "Bar", Value: "bir", Kind: reflect.String}, }}, }, }, }, }, { desc: "level 2, struct without children", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo"}, }, }, structure: struct { Foo struct { Bar string } }{ Foo: struct { Bar string }{}, }, expected: expected{error: true}, }, { desc: "level 2, slice-as-struct", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Fii", Children: []*Node{ {Name: "bar", Value: "haa"}, {Name: "bir", Value: "hii"}, }}, }, }, structure: struct { Foo []struct { Bar string Bir string } `label-slice-as-struct:"Fii"` }{ Foo: []struct { Bar string Bir string }{}, }, expected: expected{node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Fii", FieldName: "Foo", Kind: reflect.Slice, Tag: reflect.StructTag(`label-slice-as-struct:"Fii"`), Children: []*Node{ {Name: "bar", FieldName: "Bar", Kind: reflect.String, Value: "haa"}, {Name: "bir", FieldName: "Bir", Kind: reflect.String, Value: "hii"}, }, }, }, }}, }, { desc: "level 2, slice-as-struct without children", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Fii"}, }, }, structure: struct { Foo []struct { Bar string Bir string } `label-slice-as-struct:"Fii"` }{ Foo: []struct { Bar string Bir string }{}, }, expected: expected{node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Fii", FieldName: "Foo", Kind: reflect.Slice, Tag: reflect.StructTag(`label-slice-as-struct:"Fii"`), }, }, }}, }, { desc: "level 2, struct with allowEmpty, value true", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Value: "true"}, }, }, structure: struct { Foo struct { Bar string } `label:"allowEmpty"` }{ Foo: struct { Bar string }{}, }, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "true", Kind: reflect.Struct, Tag: reflect.StructTag(`label:"allowEmpty"`)}, }, }, }, }, { desc: "level 2, struct with allowEmpty, value true with case variation", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Value: "TruE"}, }, }, structure: struct { Foo struct { Bar string } `label:"allowEmpty"` }{ Foo: struct { Bar string }{}, }, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "TruE", Kind: reflect.Struct, Tag: reflect.StructTag(`label:"allowEmpty"`)}, }, }, }, }, { desc: "level 2, struct with allowEmpty, value false", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Value: "false"}, }, }, structure: struct { Foo struct { Bar string } `label:"allowEmpty"` }{ Foo: struct { Bar string }{}, }, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Value: "false", Disabled: true, Kind: reflect.Struct, Tag: reflect.StructTag(`label:"allowEmpty"`)}, }, }, }, }, { desc: "level 2, struct with allowEmpty with children, value false", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Value: "false", Children: []*Node{ {Name: "Bar", Value: "hii"}, }}, }, }, structure: struct { Foo struct { Bar string } `label:"allowEmpty"` }{ Foo: struct { Bar string }{}, }, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Foo", FieldName: "Foo", Value: "false", Disabled: true, Kind: reflect.Struct, Tag: reflect.StructTag(`label:"allowEmpty"`), Children: []*Node{ {Name: "Bar", FieldName: "Bar", Value: "hii", Kind: reflect.String}, }, }, }, }, }, }, { desc: "level 2, struct pointer without children", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo"}, }, }, structure: struct { Foo *struct { Bar string } }{ Foo: &struct { Bar string }{}, }, expected: expected{error: true}, }, { desc: "level 2, map without children", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo"}, }, }, structure: struct { Foo map[string]string }{ Foo: map[string]string{}, }, expected: expected{error: true}, }, { desc: "level 2, pointer", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Children: []*Node{ {Name: "Bar", Value: "bir"}, }}, }, }, structure: struct { Foo *struct { Bar string } }{ Foo: &struct { Bar string }{}, }, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Kind: reflect.Pointer, Children: []*Node{ {Name: "Bar", FieldName: "Bar", Value: "bir", Kind: reflect.String}, }}, }, }, }, }, { desc: "level 2, 2 children", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Children: []*Node{ {Name: "Bar", Value: "bir"}, {Name: "Bur", Value: "fuu"}, }}, }, }, structure: struct { Foo struct { Bar string Bur string } }{ Foo: struct { Bar string Bur string }{}, }, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Foo", FieldName: "Foo", Kind: reflect.Struct, Children: []*Node{ {Name: "Bar", FieldName: "Bar", Value: "bir", Kind: reflect.String}, {Name: "Bur", FieldName: "Bur", Value: "fuu", Kind: reflect.String}, }, }, }, }, }, }, { desc: "level 3", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Children: []*Node{ {Name: "Bar", Children: []*Node{ {Name: "Bur", Value: "fuu"}, }}, }}, }, }, structure: struct { Foo struct { Bar struct { Bur string } } }{ Foo: struct { Bar struct { Bur string } }{}, }, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Foo", FieldName: "Foo", Kind: reflect.Struct, Children: []*Node{ { Name: "Bar", FieldName: "Bar", Kind: reflect.Struct, Children: []*Node{ {Name: "Bur", FieldName: "Bur", Value: "fuu", Kind: reflect.String}, }, }, }, }, }, }, }, }, { desc: "level 3, 2 children level 1, 2 children level 2, 2 children level 3", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Children: []*Node{ {Name: "Bar", Children: []*Node{ {Name: "Fii", Value: "fii"}, {Name: "Fee", Value: "1"}, }}, {Name: "Bur", Children: []*Node{ {Name: "Faa", Value: "faa"}, }}, }}, {Name: "Fii", Children: []*Node{ {Name: "FiiBar", Value: "fiiBar"}, }}, }, }, structure: struct { Foo struct { Bar struct { Fii string Fee int } Bur struct { Faa string } } Fii struct { FiiBar string } }{ Foo: struct { Bar struct { Fii string Fee int } Bur struct { Faa string } }{}, Fii: struct { FiiBar string }{}, }, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ { Name: "Foo", FieldName: "Foo", Kind: reflect.Struct, Children: []*Node{ { Name: "Bar", FieldName: "Bar", Kind: reflect.Struct, Children: []*Node{ {Name: "Fii", FieldName: "Fii", Kind: reflect.String, Value: "fii"}, {Name: "Fee", FieldName: "Fee", Kind: reflect.Int, Value: "1"}, }, }, { Name: "Bur", FieldName: "Bur", Kind: reflect.Struct, Children: []*Node{ {Name: "Faa", FieldName: "Faa", Kind: reflect.String, Value: "faa"}, }, }, }, }, { Name: "Fii", FieldName: "Fii", Kind: reflect.Struct, Children: []*Node{ {Name: "FiiBar", FieldName: "FiiBar", Kind: reflect.String, Value: "fiiBar"}, }, }, }, }, }, }, { desc: "Slice struct", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Children: []*Node{ {Name: "[0]", Children: []*Node{ {Name: "Field1", Value: "A"}, {Name: "Field2", Value: "A"}, }}, {Name: "[1]", Children: []*Node{ {Name: "Field1", Value: "B"}, {Name: "Field2", Value: "B"}, }}, {Name: "[2]", Children: []*Node{ {Name: "Field1", Value: "C"}, {Name: "Field2", Value: "C"}, }}, }}, }, }, structure: struct { Foo []struct { Field1 string Field2 string } }{}, expected: expected{node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Kind: reflect.Slice, Children: []*Node{ {Name: "[0]", Kind: reflect.Struct, Children: []*Node{ {Name: "Field1", FieldName: "Field1", Value: "A", Kind: reflect.String}, {Name: "Field2", FieldName: "Field2", Value: "A", Kind: reflect.String}, }}, {Name: "[1]", Kind: reflect.Struct, Children: []*Node{ {Name: "Field1", FieldName: "Field1", Value: "B", Kind: reflect.String}, {Name: "Field2", FieldName: "Field2", Value: "B", Kind: reflect.String}, }}, {Name: "[2]", Kind: reflect.Struct, Children: []*Node{ {Name: "Field1", FieldName: "Field1", Value: "C", Kind: reflect.String}, {Name: "Field2", FieldName: "Field2", Value: "C", Kind: reflect.String}, }}, }}, }, }}, }, { desc: "Slice pointer struct", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Children: []*Node{ {Name: "[0]", Children: []*Node{ {Name: "Field1", Value: "A"}, {Name: "Field2", Value: "A"}, }}, {Name: "[1]", Children: []*Node{ {Name: "Field1", Value: "B"}, {Name: "Field2", Value: "B"}, }}, {Name: "[2]", Children: []*Node{ {Name: "Field1", Value: "C"}, {Name: "Field2", Value: "C"}, }}, }}, }, }, structure: struct { Foo []*struct { Field1 string Field2 string } }{}, expected: expected{node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Kind: reflect.Slice, Children: []*Node{ {Name: "[0]", Kind: reflect.Pointer, Children: []*Node{ {Name: "Field1", FieldName: "Field1", Value: "A", Kind: reflect.String}, {Name: "Field2", FieldName: "Field2", Value: "A", Kind: reflect.String}, }}, {Name: "[1]", Kind: reflect.Pointer, Children: []*Node{ {Name: "Field1", FieldName: "Field1", Value: "B", Kind: reflect.String}, {Name: "Field2", FieldName: "Field2", Value: "B", Kind: reflect.String}, }}, {Name: "[2]", Kind: reflect.Pointer, Children: []*Node{ {Name: "Field1", FieldName: "Field1", Value: "C", Kind: reflect.String}, {Name: "Field2", FieldName: "Field2", Value: "C", Kind: reflect.String}, }}, }}, }, }}, }, { desc: "invalid slice type", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Children: []*Node{ {Name: "[0]", Children: []*Node{ {Name: "Field1", Value: "A"}, }}, }}, }, }, structure: struct { Foo []string }{}, expected: expected{error: true}, }, { desc: "embedded", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Children: []*Node{ {Name: "Fii", Value: "bir"}, {Name: "Fuu", Value: "bur"}, }}, }, }, structure: struct { Foo struct { FiiFoo } }{}, expected: expected{node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Kind: reflect.Struct, Children: []*Node{ {Name: "Fii", FieldName: "Fii", Value: "bir", Kind: reflect.String}, {Name: "Fuu", FieldName: "Fuu", Value: "bur", Kind: reflect.String}, }}, }, }}, }, { desc: "embedded slice", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "MySliceType", Value: "foo,fii"}, }, }, structure: struct { MySliceType }{}, expected: expected{node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "MySliceType", FieldName: "MySliceType", Value: "foo,fii", Kind: reflect.Slice}, }, }}, }, { desc: "embedded slice 2", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", Children: []*Node{ {Name: "MySliceType", Value: "foo,fii"}, }}, }, }, structure: struct { Foo struct { MySliceType } }{}, expected: expected{node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Kind: reflect.Struct, Children: []*Node{ {Name: "MySliceType", FieldName: "MySliceType", Value: "foo,fii", Kind: reflect.Slice}, }}, }, }}, }, { desc: "raw value", tree: &Node{ Name: "traefik", Children: []*Node{ {Name: "Foo", FieldName: "Foo", Children: []*Node{ {Name: "Bar", FieldName: "Bar", Children: []*Node{ {Name: "AAA", FieldName: "AAA", Value: "valueA"}, {Name: "BBB", FieldName: "BBB", Children: []*Node{ {Name: "CCC", FieldName: "CCC", Children: []*Node{ {Name: "DDD", FieldName: "DDD", Value: "valueD"}, }}, }}, }}, }}, }, }, structure: struct { Foo *struct { Bar map[string]interface{} } }{ Foo: &struct { Bar map[string]interface{} }{}, }, expected: expected{ node: &Node{ Name: "traefik", Kind: reflect.Struct, Children: []*Node{ {Name: "Foo", FieldName: "Foo", Kind: reflect.Pointer, Children: []*Node{ {Name: "Bar", FieldName: "Bar", Kind: reflect.Map, RawValue: map[string]interface{}{ "AAA": "valueA", "BBB": map[string]interface{}{ "CCC": map[string]interface{}{ "DDD": "valueD", }, }, }}, }}, }, }, }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() err := metadata{MetadataOpts{TagName: TagLabel, AllowSliceAsStruct: true}}.Add(test.structure, test.tree) if test.expected.error { assert.Error(t, err) } else { require.NoError(t, err) if !assert.Equal(t, test.expected.node, test.tree) { bytes, errM := json.MarshalIndent(test.tree, "", " ") require.NoError(t, errM) t.Log(string(bytes)) } } }) } } func Test_nodeToRawMap(t *testing.T) { testCases := []struct { desc string root *Node expected map[string]interface{} }{ { desc: "simple", root: &Node{ Name: "traefik", Children: []*Node{ {Name: "meta", Children: []*Node{ {Name: "aaa", Value: "test1"}, {Name: "bbb", Children: []*Node{ {Name: "ccc", Value: "test2"}, {Name: "ddd", Children: []*Node{ {Name: "eee", Value: "test3"}, }}, }}, }}, {Name: "name", Value: "bla"}, }, }, expected: map[string]interface{}{ "meta": map[string]interface{}{ "aaa": "test1", "bbb": map[string]interface{}{ "ccc": "test2", "ddd": map[string]interface{}{ "eee": "test3", }, }, }, "name": "bla", }, }, { desc: "slice of struct, level 1", root: &Node{ Name: "aaa", Children: []*Node{ {Name: "[0]", Children: []*Node{ {Name: "bbb", Value: "test1"}, {Name: "ccc", Value: "test2"}, }}, }, }, expected: map[string]interface{}{ "aaa": []interface{}{ map[string]interface{}{ "bbb": "test1", "ccc": "test2", }, }, }, }, { desc: "slice of struct, level 2", root: &Node{ Name: "traefik", Children: []*Node{ {Name: "meta", Children: []*Node{{ Name: "aaa", Children: []*Node{ {Name: "[0]", Children: []*Node{ {Name: "bbb", Value: "test2"}, {Name: "ccc", Value: "test3"}, }}, {Name: "[1]", Children: []*Node{ {Name: "bbb", Value: "test4"}, {Name: "ccc", Value: "test5"}, }}, }, }}}, {Name: "name", Value: "test1"}, }, }, expected: map[string]interface{}{ "meta": map[string]interface{}{ "aaa": []interface{}{ map[string]interface{}{ "bbb": "test2", "ccc": "test3", }, map[string]interface{}{ "bbb": "test4", "ccc": "test5", }, }, }, "name": "test1", }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() actual := nodeToRawMap(test.root) assert.Equal(t, test.expected, actual) }) } } paerser-0.2.0/parser/parser.go000066400000000000000000000024101436254333400162750ustar00rootroot00000000000000// Package parser implements decoding and encoding between a flat map of labels and a typed Configuration. package parser // Decode decodes the given map of labels into the given element. // If any filters are present, labels which do not match the filters are skipped. // The operation goes through three stages roughly summarized as: // labels -> tree of untyped nodes // untyped nodes -> nodes augmented with metadata such as kind (inferred from element) // "typed" nodes -> typed element. func Decode(labels map[string]string, element interface{}, rootName string, filters ...string) error { node, err := DecodeToNode(labels, rootName, filters...) if err != nil { return err } metaOpts := MetadataOpts{TagName: TagLabel, AllowSliceAsStruct: true} err = AddMetadata(element, node, metaOpts) if err != nil { return err } return Fill(element, node, FillerOpts{AllowSliceAsStruct: true}) } // Encode converts an element to labels. // element -> node (value) -> label (node). func Encode(element interface{}, rootName string) (map[string]string, error) { etnOpts := EncoderToNodeOpts{OmitEmpty: true, TagName: TagLabel, AllowSliceAsStruct: true} node, err := EncodeToNode(element, rootName, etnOpts) if err != nil { return nil, err } return EncodeNode(node), nil } paerser-0.2.0/parser/parser_test.go000066400000000000000000000213201436254333400173350ustar00rootroot00000000000000package parser import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type Tomato struct { Name string Meta map[string]interface{} } type Potato struct { Name string Meta map[string]map[string]interface{} } type Ignored struct { String string `label:"-"` StringP *string `label:"-"` Struct Tomato `label:"-"` StructP *Tomato `label:"-"` Slice []string `label:"-"` Map map[string]interface{} `label:"-"` } func TestDecode_RawValue(t *testing.T) { testCases := []struct { desc string labels map[string]string elt interface{} expected interface{} }{ { desc: "level 1", elt: &Tomato{}, labels: map[string]string{ "traefik.name": "test", "traefik.meta.aaa": "test", }, expected: &Tomato{ Name: "test", Meta: map[string]interface{}{ "aaa": "test", }, }, }, { desc: "level 2", labels: map[string]string{ "traefik.name": "test", "traefik.meta.aaa": "test", "traefik.meta.bbb.ccc": "test", }, elt: &Tomato{}, expected: &Tomato{ Name: "test", Meta: map[string]interface{}{ "aaa": "test", "bbb": map[string]interface{}{ "ccc": "test", }, }, }, }, { desc: "level 3", labels: map[string]string{ "traefik.name": "test", "traefik.meta.aaa": "test", "traefik.meta.bbb.ccc": "test", "traefik.meta.bbb.ddd.eee": "test", }, elt: &Tomato{}, expected: &Tomato{ Name: "test", Meta: map[string]interface{}{ "aaa": "test", "bbb": map[string]interface{}{ "ccc": "test", "ddd": map[string]interface{}{ "eee": "test", }, }, }, }, }, { desc: "struct slice, one entry", elt: &Tomato{}, labels: map[string]string{ "traefik.name": "test1", "traefik.meta.aaa[0].bbb": "test2", "traefik.meta.aaa[0].ccc": "test3", }, expected: &Tomato{ Name: "test1", Meta: map[string]interface{}{ "aaa": []interface{}{ map[string]interface{}{ "bbb": "test2", "ccc": "test3", }, }, }, }, }, { desc: "struct slice, multiple entries", elt: &Tomato{}, labels: map[string]string{ "traefik.name": "test1", "traefik.meta.aaa[0].bbb": "test2", "traefik.meta.aaa[0].ccc": "test3", "traefik.meta.aaa[1].bbb": "test4", "traefik.meta.aaa[1].ccc": "test5", "traefik.meta.aaa[2].bbb": "test6", "traefik.meta.aaa[2].ccc": "test7", }, expected: &Tomato{ Name: "test1", Meta: map[string]interface{}{ "aaa": []interface{}{ map[string]interface{}{ "bbb": "test2", "ccc": "test3", }, map[string]interface{}{ "bbb": "test4", "ccc": "test5", }, map[string]interface{}{ "bbb": "test6", "ccc": "test7", }, }, }, }, }, { desc: "explicit map of map, level 1", elt: &Potato{}, labels: map[string]string{ "traefik.name": "test", "traefik.meta.aaa.bbb": "test1", }, expected: &Potato{ Name: "test", Meta: map[string]map[string]interface{}{ "aaa": { "bbb": "test1", }, }, }, }, { desc: "explicit map of map, level 2", elt: &Potato{}, labels: map[string]string{ "traefik.name": "test", "traefik.meta.aaa.bbb": "test1", "traefik.meta.aaa.ccc": "test2", }, expected: &Potato{ Name: "test", Meta: map[string]map[string]interface{}{ "aaa": { "bbb": "test1", "ccc": "test2", }, }, }, }, { desc: "explicit map of map, level 3", elt: &Potato{}, labels: map[string]string{ "traefik.name": "test", "traefik.meta.aaa.bbb.ccc": "test1", "traefik.meta.aaa.bbb.ddd": "test2", "traefik.meta.aaa.eee": "test3", }, expected: &Potato{ Name: "test", Meta: map[string]map[string]interface{}{ "aaa": { "bbb": map[string]interface{}{ "ccc": "test1", "ddd": "test2", }, "eee": "test3", }, }, }, }, { desc: "explicit map of map, level 4", elt: &Potato{}, labels: map[string]string{ "traefik.name": "test", "traefik.meta.aaa.bbb.ccc.ddd": "test1", "traefik.meta.aaa.bbb.ccc.eee": "test2", "traefik.meta.aaa.bbb.fff": "test3", "traefik.meta.aaa.ggg": "test4", }, expected: &Potato{ Name: "test", Meta: map[string]map[string]interface{}{ "aaa": { "bbb": map[string]interface{}{ "ccc": map[string]interface{}{ "ddd": "test1", "eee": "test2", }, "fff": "test3", }, "ggg": "test4", }, }, }, }, { desc: "explicit map of map, struct slice, level 1, one entry", elt: &Potato{}, labels: map[string]string{ "traefik.name": "test1", "traefik.meta.aaa.bbb[0].ccc": "test2", "traefik.meta.aaa.bbb[0].ddd": "test3", }, expected: &Potato{ Name: "test1", Meta: map[string]map[string]interface{}{ "aaa": { "bbb": []interface{}{ map[string]interface{}{ "ccc": "test2", "ddd": "test3", }, }, }, }, }, }, { desc: "explicit map of map, struct slice, level 1, multiple entries", elt: &Potato{}, labels: map[string]string{ "traefik.name": "test1", "traefik.meta.aaa.bbb[0].ccc": "test2", "traefik.meta.aaa.bbb[0].ddd": "test3", "traefik.meta.aaa.bbb[1].ccc": "test4", "traefik.meta.aaa.bbb[1].ddd": "test5", "traefik.meta.aaa.bbb[2].ccc": "test6", "traefik.meta.aaa.bbb[2].ddd": "test7", }, expected: &Potato{ Name: "test1", Meta: map[string]map[string]interface{}{ "aaa": { "bbb": []interface{}{ map[string]interface{}{ "ccc": "test2", "ddd": "test3", }, map[string]interface{}{ "ccc": "test4", "ddd": "test5", }, map[string]interface{}{ "ccc": "test6", "ddd": "test7", }, }, }, }, }, }, { desc: "explicit map of map, struct slice, level 2, one entry", elt: &Potato{}, labels: map[string]string{ "traefik.name": "test1", "traefik.meta.aaa.bbb.ccc[0].ddd": "test2", "traefik.meta.aaa.bbb.ccc[0].eee": "test3", }, expected: &Potato{ Name: "test1", Meta: map[string]map[string]interface{}{ "aaa": { "bbb": map[string]interface{}{ "ccc": []interface{}{ map[string]interface{}{ "ddd": "test2", "eee": "test3", }, }, }, }, }, }, }, { desc: "explicit map of map, struct slice, level 2, multiple entries", elt: &Potato{}, labels: map[string]string{ "traefik.name": "test1", "traefik.meta.aaa.bbb.ccc[0].ddd": "test2", "traefik.meta.aaa.bbb.ccc[0].eee": "test3", "traefik.meta.aaa.bbb.ccc[1].ddd": "test4", "traefik.meta.aaa.bbb.ccc[1].eee": "test5", "traefik.meta.aaa.bbb.ccc[2].ddd": "test6", "traefik.meta.aaa.bbb.ccc[2].eee": "test7", }, expected: &Potato{ Name: "test1", Meta: map[string]map[string]interface{}{ "aaa": { "bbb": map[string]interface{}{ "ccc": []interface{}{ map[string]interface{}{ "ddd": "test2", "eee": "test3", }, map[string]interface{}{ "ddd": "test4", "eee": "test5", }, map[string]interface{}{ "ddd": "test6", "eee": "test7", }, }, }, }, }, }, }, { desc: "Ignore label tag, one element label for each type", elt: &Ignored{}, labels: map[string]string{ "traefik.string": "test1", "traefik.stringp": "test1", "traefik.struct.name": "test1", "traefik.structp.name": "test1", "traefik.slice": "test1", "traefik.map.aaa": "test1", }, expected: &Ignored{ String: "", StringP: nil, Struct: Tomato{}, StructP: nil, Slice: nil, Map: nil, }, }, { desc: "Ignore label tag, one empty label for each type", elt: &Ignored{}, labels: map[string]string{ "traefik.string": "", "traefik.stringp": "", "traefik.struct": "", "traefik.structp": "", "traefik.slice": "", "traefik.map": "", }, expected: &Ignored{ String: "", StringP: nil, Struct: Tomato{}, StructP: nil, Slice: nil, Map: nil, }, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { err := Decode(test.labels, test.elt, "traefik") require.NoError(t, err) assert.Equal(t, test.expected, test.elt) }) } } paerser-0.2.0/parser/tags.go000066400000000000000000000016641436254333400157510ustar00rootroot00000000000000package parser const ( // TagLabel allows to apply a custom behavior. // - "allowEmpty": allows the creation of a type that is supposed to have children // (i.e: struct, pointer of struct, and map), without any children. // - "-": ignore the field. TagLabel = "label" // TagFile allows to apply a custom behavior. // - "allowEmpty": allows the creation of a type that is supposed to have children // (i.e: struct, pointer of struct, and map), without any children. // - "-": ignore the field. TagFile = "file" // TagLabelSliceAsStruct allows to use a slice of struct by creating one entry into the slice. // The value is the substitution name used in the label to access the slice. TagLabelSliceAsStruct = "label-slice-as-struct" // TagDescription is the documentation for the field. // - "-": ignore the field. TagDescription = "description" // TagLabelAllowEmpty is related to TagLabel. TagLabelAllowEmpty = "allowEmpty" ) paerser-0.2.0/readme.md000066400000000000000000000100711436254333400147370ustar00rootroot00000000000000# Paerser [![Package documentation](https://img.shields.io/badge/go.dev-docs-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/traefik/paerser) [![Build Status](https://github.com/traefik/paerser/workflows/Main/badge.svg?branch=master)](https://github.com/traefik/paerser/actions) [![Go Report Card](https://goreportcard.com/badge/github.com/traefik/paerser)](https://goreportcard.com/report/github.com/traefik/paerser) ## Features Loads configuration from many sources: - CLI flags - Configuration files in YAML, TOML, JSON format - Environment variables It also provides a simple CLI commands handling system. ## Examples ### Configuration #### CLI Flags ```go package flag_test import ( "log" "github.com/davecgh/go-spew/spew" "github.com/traefik/paerser/flag" ) type ConfigExample struct { Foo string Bar Bar Great bool } type Bar struct { Sub *Sub List []string } type Sub struct { Name string Value int } func ExampleDecode() { args := []string{ "--foo=aaa", "--great=true", "--bar.list=AAA,BBB", "--bar.sub.name=bbb", "--bar.sub.value=6", } config := ConfigExample{} err := flag.Decode(args, &config) if err != nil { log.Fatal(err) } spew.Config = spew.ConfigState{ Indent: "\t", DisablePointerAddresses: true, } spew.Dump(config) // Output: // (flag_test.ConfigExample) { // Foo: (string) (len=3) "aaa", // Bar: (flag_test.Bar) { // Sub: (*flag_test.Sub)({ // Name: (string) (len=3) "bbb", // Value: (int) 6 // }), // List: ([]string) (len=2 cap=2) { // (string) (len=3) "AAA", // (string) (len=3) "BBB" // } // }, // Great: (bool) true // } } ``` #### File ```go package file_test import ( "fmt" "log" "os" "github.com/davecgh/go-spew/spew" "github.com/traefik/paerser/file" ) type ConfigExample struct { Foo string Bar Bar Great bool } type Bar struct { Sub *Sub List []string } type Sub struct { Name string Value int } func ExampleDecode() { tempFile, err := os.CreateTemp("", "paeser-*.yml") if err != nil { log.Fatal(err) } defer func() { _ = os.RemoveAll(tempFile.Name()) }() data := ` foo: aaa bar: sub: name: bbb value: 6 list: - AAA - BBB great: true ` _, err = fmt.Fprint(tempFile, data) if err != nil { log.Fatal(err) } // Read configuration file filePath := tempFile.Name() config := ConfigExample{} err = file.Decode(filePath, &config) if err != nil { log.Fatal(err) } spew.Config = spew.ConfigState{ Indent: "\t", DisablePointerAddresses: true, } spew.Dump(config) // Output: // (file_test.ConfigExample) { // Foo: (string) (len=3) "aaa", // Bar: (file_test.Bar) { // Sub: (*file_test.Sub)({ // Name: (string) (len=3) "bbb", // Value: (int) 6 // }), // List: ([]string) (len=2 cap=2) { // (string) (len=3) "AAA", // (string) (len=3) "BBB" // } // }, // Great: (bool) true // } } ``` #### Environment Variables ```go package env_test import ( "log" "os" "github.com/davecgh/go-spew/spew" "github.com/traefik/paerser/env" ) type ConfigExample struct { Foo string Bar Bar Great bool } type Bar struct { Sub *Sub List []string } type Sub struct { Name string Value int } func ExampleDecode() { _ = os.Setenv("MYAPP_FOO", "aaa") _ = os.Setenv("MYAPP_GREAT", "true") _ = os.Setenv("MYAPP_BAR_LIST", "AAA,BBB") _ = os.Setenv("MYAPP_BAR_SUB_NAME", "bbb") _ = os.Setenv("MYAPP_BAR_SUB_VALUE", "6") config := ConfigExample{} err := env.Decode(os.Environ(), "MYAPP_", &config) if err != nil { log.Fatal(err) } spew.Config = spew.ConfigState{ Indent: "\t", DisablePointerAddresses: true, } spew.Dump(config) // Output: // (env_test.ConfigExample) { // Foo: (string) (len=3) "aaa", // Bar: (env_test.Bar) { // Sub: (*env_test.Sub)({ // Name: (string) (len=3) "bbb", // Value: (int) 6 // }), // List: ([]string) (len=2 cap=2) { // (string) (len=3) "AAA", // (string) (len=3) "BBB" // } // }, // Great: (bool) true // } } ``` ### CLI Commands ```go TODO ``` paerser-0.2.0/types/000077500000000000000000000000001436254333400143255ustar00rootroot00000000000000paerser-0.2.0/types/duration.go000066400000000000000000000032471436254333400165070ustar00rootroot00000000000000package types import ( "encoding/json" "strconv" "time" ) // Duration is a custom type suitable for parsing duration values. // It supports `time.ParseDuration`-compatible values and suffix-less digits; in // the latter case, seconds are assumed. type Duration time.Duration // Set sets the duration from the given string value. func (d *Duration) Set(s string) error { if v, err := strconv.ParseInt(s, 10, 64); err == nil { *d = Duration(time.Duration(v) * time.Second) return nil } v, err := time.ParseDuration(s) *d = Duration(v) return err } // String returns a string representation of the duration value. func (d Duration) String() string { return (time.Duration)(d).String() } // MarshalText serialize the given duration value into a text. func (d Duration) MarshalText() ([]byte, error) { return []byte(d.String()), nil } // UnmarshalText deserializes the given text into a duration value. // It is meant to support TOML decoding of durations. func (d *Duration) UnmarshalText(text []byte) error { return d.Set(string(text)) } // MarshalJSON serializes the given duration value. func (d Duration) MarshalJSON() ([]byte, error) { return []byte(`"` + time.Duration(d).String() + `"`), nil } // UnmarshalJSON deserializes the given text into a duration value. func (d *Duration) UnmarshalJSON(text []byte) error { if v, err := strconv.ParseInt(string(text), 10, 64); err == nil { *d = Duration(time.Duration(v) * time.Second) return nil } // We use json unmarshal on value because we have the quoted version var value string err := json.Unmarshal(text, &value) if err != nil { return err } v, err := time.ParseDuration(value) *d = Duration(v) return err } paerser-0.2.0/types/duration_test.go000066400000000000000000000054721436254333400175500ustar00rootroot00000000000000package types import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDuration_Set(t *testing.T) { testCases := []struct { desc string value string assert require.ErrorAssertionFunc expected Duration }{ { desc: "empty", value: "", assert: require.Error, }, { desc: "duration", value: "2m", assert: require.NoError, expected: Duration(2 * time.Minute), }, { desc: "integer", value: "2", assert: require.NoError, expected: Duration(2 * time.Second), }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { var d Duration err := d.Set(test.value) test.assert(t, err) assert.Equal(t, test.expected, d) }) } } func TestDuration_MarshalJSON(t *testing.T) { testCases := []struct { desc string dur Duration expected []byte }{ { desc: "1 second", dur: Duration(time.Second), expected: []byte(`"1s"`), }, { desc: "1 millisecond", dur: Duration(time.Millisecond), expected: []byte(`"1ms"`), }, { desc: "1 nanosecond", dur: Duration(time.Nanosecond), expected: []byte(`"1ns"`), }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() b, err := test.dur.MarshalJSON() require.NoError(t, err) assert.Equal(t, test.expected, b) }) } } func TestDuration_JSON_bijection(t *testing.T) { testCases := []struct { desc string dur Duration }{ { desc: "1 second", dur: Duration(time.Second), }, { desc: "1 millisecond", dur: Duration(time.Millisecond), }, { desc: "1 nanosecond", dur: Duration(time.Nanosecond), }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() b, err := test.dur.MarshalJSON() require.NoError(t, err) var ud Duration err = ud.UnmarshalJSON(b) require.NoError(t, err) assert.Equal(t, test.dur, ud) }) } } func TestDuration_UnmarshalJSON(t *testing.T) { testCases := []struct { desc string text []byte assert require.ErrorAssertionFunc expected Duration }{ { desc: "empty", text: []byte(""), assert: require.Error, }, { desc: "duration", text: []byte(`"2m"`), assert: require.NoError, expected: Duration(2 * time.Minute), }, { desc: "integer", text: []byte(`2`), assert: require.NoError, expected: Duration(2 * time.Second), }, { desc: "bad format", text: []byte(`"2"`), assert: require.Error, }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { var d Duration err := d.UnmarshalJSON(test.text) test.assert(t, err) assert.Equal(t, test.expected, d) }) } }