diff -Nru golang-github-pelletier-go-toml-1.6.0/azure-pipelines.yml golang-github-pelletier-go-toml-1.8.1/azure-pipelines.yml --- golang-github-pelletier-go-toml-1.6.0/azure-pipelines.yml 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/azure-pipelines.yml 2020-09-12 18:42:04.000000000 +0000 @@ -13,9 +13,9 @@ vmImage: ubuntu-latest steps: - task: GoTool@0 - displayName: "Install Go 1.13" + displayName: "Install Go 1.15" inputs: - version: "1.13" + version: "1.15" - script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/" - script: mkdir -p ${HOME}/go/src/github.com/pelletier/go-toml - script: cp -R . ${HOME}/go/src/github.com/pelletier/go-toml @@ -36,9 +36,9 @@ vmImage: ubuntu-latest steps: - task: GoTool@0 - displayName: "Install Go 1.13" + displayName: "Install Go 1.15" inputs: - version: "1.13" + version: "1.15" - task: Go@0 displayName: "go fmt ./..." inputs: @@ -51,9 +51,9 @@ vmImage: ubuntu-latest steps: - task: GoTool@0 - displayName: "Install Go 1.13" + displayName: "Install Go 1.15" inputs: - version: "1.13" + version: "1.15" - task: Go@0 displayName: "Generate coverage" inputs: @@ -62,16 +62,18 @@ - task: Bash@3 inputs: targetType: 'inline' - script: 'bash <(curl -s https://codecov.io/bash) -t $(CODECOV_TOKEN)' + script: 'bash <(curl -s https://codecov.io/bash) -t ${CODECOV_TOKEN}' + env: + CODECOV_TOKEN: $(CODECOV_TOKEN) - job: benchmark displayName: "benchmark" pool: vmImage: ubuntu-latest steps: - task: GoTool@0 - displayName: "Install Go 1.13" + displayName: "Install Go 1.15" inputs: - version: "1.13" + version: "1.15" - script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/" - task: Bash@3 inputs: @@ -84,9 +86,9 @@ vmImage: ubuntu-latest steps: - task: GoTool@0 - displayName: "Install Go 1.13" + displayName: "Install Go 1.15" inputs: - version: "1.13" + version: "1.15" - script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/" - script: mkdir -p ${HOME}/go/src/github.com/pelletier/go-toml - script: cp -R . ${HOME}/go/src/github.com/pelletier/go-toml @@ -100,24 +102,24 @@ displayName: "unit tests" strategy: matrix: - linux 1.13: - goVersion: '1.13' + linux 1.15: + goVersion: '1.15' imageName: 'ubuntu-latest' - mac 1.13: - goVersion: '1.13' - imageName: 'macos-10.13' - windows 1.13: - goVersion: '1.13' - imageName: 'vs2017-win2016' - linux 1.12: - goVersion: '1.12' + mac 1.15: + goVersion: '1.15' + imageName: 'macOS-latest' + windows 1.15: + goVersion: '1.15' + imageName: 'windows-latest' + linux 1.14: + goVersion: '1.14' imageName: 'ubuntu-latest' - mac 1.12: - goVersion: '1.12' - imageName: 'macos-10.13' - windows 1.12: - goVersion: '1.12' - imageName: 'vs2017-win2016' + mac 1.14: + goVersion: '1.14' + imageName: 'macOS-latest' + windows 1.14: + goVersion: '1.14' + imageName: 'windows-latest' pool: vmImage: $(imageName) steps: @@ -130,6 +132,67 @@ inputs: command: 'test' arguments: './...' +- stage: build_binaries + displayName: "Build binaries" + dependsOn: run_checks + jobs: + - job: build_binary + displayName: "Build binary" + strategy: + matrix: + linux_amd64: + GOOS: linux + GOARCH: amd64 + darwin_amd64: + GOOS: darwin + GOARCH: amd64 + windows_amd64: + GOOS: windows + GOARCH: amd64 + pool: + vmImage: ubuntu-latest + steps: + - task: GoTool@0 + displayName: "Install Go" + inputs: + version: 1.15 + - task: Bash@3 + inputs: + targetType: inline + script: "make dist" + env: + go.goos: $(GOOS) + go.goarch: $(GOARCH) + - task: CopyFiles@2 + inputs: + sourceFolder: '$(Build.SourcesDirectory)' + contents: '*.tar.xz' + TargetFolder: '$(Build.ArtifactStagingDirectory)' + - task: PublishBuildArtifacts@1 + inputs: + pathtoPublish: '$(Build.ArtifactStagingDirectory)' + artifactName: binaries +- stage: build_binaries_manifest + displayName: "Build binaries manifest" + dependsOn: build_binaries + jobs: + - job: build_manifest + displayName: "Build binaries manifest" + steps: + - task: DownloadBuildArtifacts@0 + inputs: + buildType: 'current' + downloadType: 'single' + artifactName: 'binaries' + downloadPath: '$(Build.SourcesDirectory)' + - task: Bash@3 + inputs: + targetType: inline + script: "cd binaries && sha256sum --binary *.tar.xz | tee $(Build.ArtifactStagingDirectory)/sha256sums.txt" + - task: PublishBuildArtifacts@1 + inputs: + pathtoPublish: '$(Build.ArtifactStagingDirectory)' + artifactName: manifest - stage: build_docker_image displayName: "Build Docker image" @@ -164,4 +227,4 @@ command: 'buildAndPush' Dockerfile: 'Dockerfile' buildContext: '.' - tags: 'latest' \ No newline at end of file + tags: 'latest' diff -Nru golang-github-pelletier-go-toml-1.6.0/benchmark/benchmark.json golang-github-pelletier-go-toml-1.8.1/benchmark/benchmark.json --- golang-github-pelletier-go-toml-1.6.0/benchmark/benchmark.json 1970-01-01 00:00:00.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/benchmark/benchmark.json 2020-09-12 18:42:04.000000000 +0000 @@ -0,0 +1,164 @@ +{ + "array": { + "key1": [ + 1, + 2, + 3 + ], + "key2": [ + "red", + "yellow", + "green" + ], + "key3": [ + [ + 1, + 2 + ], + [ + 3, + 4, + 5 + ] + ], + "key4": [ + [ + 1, + 2 + ], + [ + "a", + "b", + "c" + ] + ], + "key5": [ + 1, + 2, + 3 + ], + "key6": [ + 1, + 2 + ] + }, + "boolean": { + "False": false, + "True": true + }, + "datetime": { + "key1": "1979-05-27T07:32:00Z", + "key2": "1979-05-27T00:32:00-07:00", + "key3": "1979-05-27T00:32:00.999999-07:00" + }, + "float": { + "both": { + "key": 6.626e-34 + }, + "exponent": { + "key1": 5e+22, + "key2": 1000000, + "key3": -0.02 + }, + "fractional": { + "key1": 1, + "key2": 3.1415, + "key3": -0.01 + }, + "underscores": { + "key1": 9224617.445991227, + "key2": 1e+100 + } + }, + "fruit": [{ + "name": "apple", + "physical": { + "color": "red", + "shape": "round" + }, + "variety": [{ + "name": "red delicious" + }, + { + "name": "granny smith" + } + ] + }, + { + "name": "banana", + "variety": [{ + "name": "plantain" + }] + } + ], + "integer": { + "key1": 99, + "key2": 42, + "key3": 0, + "key4": -17, + "underscores": { + "key1": 1000, + "key2": 5349221, + "key3": 12345 + } + }, + "products": [{ + "name": "Hammer", + "sku": 738594937 + }, + {}, + { + "color": "gray", + "name": "Nail", + "sku": 284758393 + } + ], + "string": { + "basic": { + "basic": "I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF." + }, + "literal": { + "multiline": { + "lines": "The first newline is\ntrimmed in raw strings.\n All other whitespace\n is preserved.\n", + "regex2": "I [dw]on't need \\d{2} apples" + }, + "quoted": "Tom \"Dubs\" Preston-Werner", + "regex": "\u003c\\i\\c*\\s*\u003e", + "winpath": "C:\\Users\\nodejs\\templates", + "winpath2": "\\\\ServerX\\admin$\\system32\\" + }, + "multiline": { + "continued": { + "key1": "The quick brown fox jumps over the lazy dog.", + "key2": "The quick brown fox jumps over the lazy dog.", + "key3": "The quick brown fox jumps over the lazy dog." + }, + "key1": "One\nTwo", + "key2": "One\nTwo", + "key3": "One\nTwo" + } + }, + "table": { + "inline": { + "name": { + "first": "Tom", + "last": "Preston-Werner" + }, + "point": { + "x": 1, + "y": 2 + } + }, + "key": "value", + "subtable": { + "key": "another value" + } + }, + "x": { + "y": { + "z": { + "w": {} + } + } + } +} diff -Nru golang-github-pelletier-go-toml-1.6.0/benchmark/benchmark_test.go golang-github-pelletier-go-toml-1.8.1/benchmark/benchmark_test.go --- golang-github-pelletier-go-toml-1.6.0/benchmark/benchmark_test.go 1970-01-01 00:00:00.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/benchmark/benchmark_test.go 2020-09-12 18:42:04.000000000 +0000 @@ -0,0 +1,193 @@ +package benchmark + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "testing" + "time" + + burntsushi "github.com/BurntSushi/toml" + "github.com/pelletier/go-toml" + "gopkg.in/yaml.v2" +) + +type benchmarkDoc struct { + Table struct { + Key string + Subtable struct { + Key string + } + Inline struct { + Name struct { + First string + Last string + } + Point struct { + X int64 + U int64 + } + } + } + String struct { + Basic struct { + Basic string + } + Multiline struct { + Key1 string + Key2 string + Key3 string + Continued struct { + Key1 string + Key2 string + Key3 string + } + } + Literal struct { + Winpath string + Winpath2 string + Quoted string + Regex string + Multiline struct { + Regex2 string + Lines string + } + } + } + Integer struct { + Key1 int64 + Key2 int64 + Key3 int64 + Key4 int64 + Underscores struct { + Key1 int64 + Key2 int64 + Key3 int64 + } + } + Float struct { + Fractional struct { + Key1 float64 + Key2 float64 + Key3 float64 + } + Exponent struct { + Key1 float64 + Key2 float64 + Key3 float64 + } + Both struct { + Key float64 + } + Underscores struct { + Key1 float64 + Key2 float64 + } + } + Boolean struct { + True bool + False bool + } + Datetime struct { + Key1 time.Time + Key2 time.Time + Key3 time.Time + } + Array struct { + Key1 []int64 + Key2 []string + Key3 [][]int64 + // TODO: Key4 not supported by go-toml's Unmarshal + Key5 []int64 + Key6 []int64 + } + Products []struct { + Name string + Sku int64 + Color string + } + Fruit []struct { + Name string + Physical struct { + Color string + Shape string + Variety []struct { + Name string + } + } + } +} + +func BenchmarkParseToml(b *testing.B) { + fileBytes, err := ioutil.ReadFile("benchmark.toml") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := toml.LoadReader(bytes.NewReader(fileBytes)) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkUnmarshalToml(b *testing.B) { + bytes, err := ioutil.ReadFile("benchmark.toml") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + target := benchmarkDoc{} + err := toml.Unmarshal(bytes, &target) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkUnmarshalBurntSushiToml(b *testing.B) { + bytes, err := ioutil.ReadFile("benchmark.toml") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + target := benchmarkDoc{} + err := burntsushi.Unmarshal(bytes, &target) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkUnmarshalJson(b *testing.B) { + bytes, err := ioutil.ReadFile("benchmark.json") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + target := benchmarkDoc{} + err := json.Unmarshal(bytes, &target) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkUnmarshalYaml(b *testing.B) { + bytes, err := ioutil.ReadFile("benchmark.yml") + if err != nil { + b.Fatal(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + target := benchmarkDoc{} + err := yaml.Unmarshal(bytes, &target) + if err != nil { + b.Fatal(err) + } + } +} diff -Nru golang-github-pelletier-go-toml-1.6.0/benchmark/benchmark.toml golang-github-pelletier-go-toml-1.8.1/benchmark/benchmark.toml --- golang-github-pelletier-go-toml-1.6.0/benchmark/benchmark.toml 1970-01-01 00:00:00.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/benchmark/benchmark.toml 2020-09-12 18:42:04.000000000 +0000 @@ -0,0 +1,244 @@ +################################################################################ +## Comment + +# Speak your mind with the hash symbol. They go from the symbol to the end of +# the line. + + +################################################################################ +## Table + +# Tables (also known as hash tables or dictionaries) are collections of +# key/value pairs. They appear in square brackets on a line by themselves. + +[table] + +key = "value" # Yeah, you can do this. + +# Nested tables are denoted by table names with dots in them. Name your tables +# whatever crap you please, just don't use #, ., [ or ]. + +[table.subtable] + +key = "another value" + +# You don't need to specify all the super-tables if you don't want to. TOML +# knows how to do it for you. + +# [x] you +# [x.y] don't +# [x.y.z] need these +[x.y.z.w] # for this to work + + +################################################################################ +## Inline Table + +# Inline tables provide a more compact syntax for expressing tables. They are +# especially useful for grouped data that can otherwise quickly become verbose. +# Inline tables are enclosed in curly braces `{` and `}`. No newlines are +# allowed between the curly braces unless they are valid within a value. + +[table.inline] + +name = { first = "Tom", last = "Preston-Werner" } +point = { x = 1, y = 2 } + + +################################################################################ +## String + +# There are four ways to express strings: basic, multi-line basic, literal, and +# multi-line literal. All strings must contain only valid UTF-8 characters. + +[string.basic] + +basic = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF." + +[string.multiline] + +# The following strings are byte-for-byte equivalent: +key1 = "One\nTwo" +key2 = """One\nTwo""" +key3 = """ +One +Two""" + +[string.multiline.continued] + +# The following strings are byte-for-byte equivalent: +key1 = "The quick brown fox jumps over the lazy dog." + +key2 = """ +The quick brown \ + + + fox jumps over \ + the lazy dog.""" + +key3 = """\ + The quick brown \ + fox jumps over \ + the lazy dog.\ + """ + +[string.literal] + +# What you see is what you get. +winpath = 'C:\Users\nodejs\templates' +winpath2 = '\\ServerX\admin$\system32\' +quoted = 'Tom "Dubs" Preston-Werner' +regex = '<\i\c*\s*>' + + +[string.literal.multiline] + +regex2 = '''I [dw]on't need \d{2} apples''' +lines = ''' +The first newline is +trimmed in raw strings. + All other whitespace + is preserved. +''' + + +################################################################################ +## Integer + +# Integers are whole numbers. Positive numbers may be prefixed with a plus sign. +# Negative numbers are prefixed with a minus sign. + +[integer] + +key1 = +99 +key2 = 42 +key3 = 0 +key4 = -17 + +[integer.underscores] + +# For large numbers, you may use underscores to enhance readability. Each +# underscore must be surrounded by at least one digit. +key1 = 1_000 +key2 = 5_349_221 +key3 = 1_2_3_4_5 # valid but inadvisable + + +################################################################################ +## Float + +# A float consists of an integer part (which may be prefixed with a plus or +# minus sign) followed by a fractional part and/or an exponent part. + +[float.fractional] + +key1 = +1.0 +key2 = 3.1415 +key3 = -0.01 + +[float.exponent] + +key1 = 5e+22 +key2 = 1e6 +key3 = -2E-2 + +[float.both] + +key = 6.626e-34 + +[float.underscores] + +key1 = 9_224_617.445_991_228_313 +key2 = 1e1_00 + + +################################################################################ +## Boolean + +# Booleans are just the tokens you're used to. Always lowercase. + +[boolean] + +True = true +False = false + + +################################################################################ +## Datetime + +# Datetimes are RFC 3339 dates. + +[datetime] + +key1 = 1979-05-27T07:32:00Z +key2 = 1979-05-27T00:32:00-07:00 +key3 = 1979-05-27T00:32:00.999999-07:00 + + +################################################################################ +## Array + +# Arrays are square brackets with other primitives inside. Whitespace is +# ignored. Elements are separated by commas. Data types may not be mixed. + +[array] + +key1 = [ 1, 2, 3 ] +key2 = [ "red", "yellow", "green" ] +key3 = [ [ 1, 2 ], [3, 4, 5] ] +#key4 = [ [ 1, 2 ], ["a", "b", "c"] ] # this is ok + +# Arrays can also be multiline. So in addition to ignoring whitespace, arrays +# also ignore newlines between the brackets. Terminating commas are ok before +# the closing bracket. + +key5 = [ + 1, 2, 3 +] +key6 = [ + 1, + 2, # this is ok +] + + +################################################################################ +## Array of Tables + +# These can be expressed by using a table name in double brackets. Each table +# with the same double bracketed name will be an element in the array. The +# tables are inserted in the order encountered. + +[[products]] + +name = "Hammer" +sku = 738594937 + +[[products]] + +[[products]] + +name = "Nail" +sku = 284758393 +color = "gray" + + +# You can create nested arrays of tables as well. + +[[fruit]] + name = "apple" + + [fruit.physical] + color = "red" + shape = "round" + + [[fruit.variety]] + name = "red delicious" + + [[fruit.variety]] + name = "granny smith" + +[[fruit]] + name = "banana" + + [[fruit.variety]] + name = "plantain" diff -Nru golang-github-pelletier-go-toml-1.6.0/benchmark/benchmark.yml golang-github-pelletier-go-toml-1.8.1/benchmark/benchmark.yml --- golang-github-pelletier-go-toml-1.6.0/benchmark/benchmark.yml 1970-01-01 00:00:00.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/benchmark/benchmark.yml 2020-09-12 18:42:04.000000000 +0000 @@ -0,0 +1,121 @@ +--- +array: + key1: + - 1 + - 2 + - 3 + key2: + - red + - yellow + - green + key3: + - - 1 + - 2 + - - 3 + - 4 + - 5 + key4: + - - 1 + - 2 + - - a + - b + - c + key5: + - 1 + - 2 + - 3 + key6: + - 1 + - 2 +boolean: + 'False': false + 'True': true +datetime: + key1: '1979-05-27T07:32:00Z' + key2: '1979-05-27T00:32:00-07:00' + key3: '1979-05-27T00:32:00.999999-07:00' +float: + both: + key: 6.626e-34 + exponent: + key1: 5.0e+22 + key2: 1000000 + key3: -0.02 + fractional: + key1: 1 + key2: 3.1415 + key3: -0.01 + underscores: + key1: 9224617.445991227 + key2: 1.0e+100 +fruit: +- name: apple + physical: + color: red + shape: round + variety: + - name: red delicious + - name: granny smith +- name: banana + variety: + - name: plantain +integer: + key1: 99 + key2: 42 + key3: 0 + key4: -17 + underscores: + key1: 1000 + key2: 5349221 + key3: 12345 +products: +- name: Hammer + sku: 738594937 +- {} +- color: gray + name: Nail + sku: 284758393 +string: + basic: + basic: "I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF." + literal: + multiline: + lines: | + The first newline is + trimmed in raw strings. + All other whitespace + is preserved. + regex2: I [dw]on't need \d{2} apples + quoted: Tom "Dubs" Preston-Werner + regex: "<\\i\\c*\\s*>" + winpath: C:\Users\nodejs\templates + winpath2: "\\\\ServerX\\admin$\\system32\\" + multiline: + continued: + key1: The quick brown fox jumps over the lazy dog. + key2: The quick brown fox jumps over the lazy dog. + key3: The quick brown fox jumps over the lazy dog. + key1: |- + One + Two + key2: |- + One + Two + key3: |- + One + Two +table: + inline: + name: + first: Tom + last: Preston-Werner + point: + x: 1 + y: 2 + key: value + subtable: + key: another value +x: + y: + z: + w: {} diff -Nru golang-github-pelletier-go-toml-1.6.0/benchmark/go.mod golang-github-pelletier-go-toml-1.8.1/benchmark/go.mod --- golang-github-pelletier-go-toml-1.6.0/benchmark/go.mod 1970-01-01 00:00:00.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/benchmark/go.mod 2020-09-12 18:42:04.000000000 +0000 @@ -0,0 +1,11 @@ +module github.com/pelletier/go-toml/benchmark + +go 1.12 + +require ( + github.com/BurntSushi/toml v0.3.1 + github.com/pelletier/go-toml v0.0.0 + gopkg.in/yaml.v2 v2.3.0 +) + +replace github.com/pelletier/go-toml => ../ diff -Nru golang-github-pelletier-go-toml-1.6.0/benchmark/go.sum golang-github-pelletier-go-toml-1.8.1/benchmark/go.sum --- golang-github-pelletier-go-toml-1.6.0/benchmark/go.sum 1970-01-01 00:00:00.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/benchmark/go.sum 2020-09-12 18:42:04.000000000 +0000 @@ -0,0 +1,8 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +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= +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.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff -Nru golang-github-pelletier-go-toml-1.6.0/benchmark.json golang-github-pelletier-go-toml-1.8.1/benchmark.json --- golang-github-pelletier-go-toml-1.6.0/benchmark.json 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/benchmark.json 1970-01-01 00:00:00.000000000 +0000 @@ -1,164 +0,0 @@ -{ - "array": { - "key1": [ - 1, - 2, - 3 - ], - "key2": [ - "red", - "yellow", - "green" - ], - "key3": [ - [ - 1, - 2 - ], - [ - 3, - 4, - 5 - ] - ], - "key4": [ - [ - 1, - 2 - ], - [ - "a", - "b", - "c" - ] - ], - "key5": [ - 1, - 2, - 3 - ], - "key6": [ - 1, - 2 - ] - }, - "boolean": { - "False": false, - "True": true - }, - "datetime": { - "key1": "1979-05-27T07:32:00Z", - "key2": "1979-05-27T00:32:00-07:00", - "key3": "1979-05-27T00:32:00.999999-07:00" - }, - "float": { - "both": { - "key": 6.626e-34 - }, - "exponent": { - "key1": 5e+22, - "key2": 1000000, - "key3": -0.02 - }, - "fractional": { - "key1": 1, - "key2": 3.1415, - "key3": -0.01 - }, - "underscores": { - "key1": 9224617.445991227, - "key2": 1e+100 - } - }, - "fruit": [{ - "name": "apple", - "physical": { - "color": "red", - "shape": "round" - }, - "variety": [{ - "name": "red delicious" - }, - { - "name": "granny smith" - } - ] - }, - { - "name": "banana", - "variety": [{ - "name": "plantain" - }] - } - ], - "integer": { - "key1": 99, - "key2": 42, - "key3": 0, - "key4": -17, - "underscores": { - "key1": 1000, - "key2": 5349221, - "key3": 12345 - } - }, - "products": [{ - "name": "Hammer", - "sku": 738594937 - }, - {}, - { - "color": "gray", - "name": "Nail", - "sku": 284758393 - } - ], - "string": { - "basic": { - "basic": "I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF." - }, - "literal": { - "multiline": { - "lines": "The first newline is\ntrimmed in raw strings.\n All other whitespace\n is preserved.\n", - "regex2": "I [dw]on't need \\d{2} apples" - }, - "quoted": "Tom \"Dubs\" Preston-Werner", - "regex": "\u003c\\i\\c*\\s*\u003e", - "winpath": "C:\\Users\\nodejs\\templates", - "winpath2": "\\\\ServerX\\admin$\\system32\\" - }, - "multiline": { - "continued": { - "key1": "The quick brown fox jumps over the lazy dog.", - "key2": "The quick brown fox jumps over the lazy dog.", - "key3": "The quick brown fox jumps over the lazy dog." - }, - "key1": "One\nTwo", - "key2": "One\nTwo", - "key3": "One\nTwo" - } - }, - "table": { - "inline": { - "name": { - "first": "Tom", - "last": "Preston-Werner" - }, - "point": { - "x": 1, - "y": 2 - } - }, - "key": "value", - "subtable": { - "key": "another value" - } - }, - "x": { - "y": { - "z": { - "w": {} - } - } - } -} diff -Nru golang-github-pelletier-go-toml-1.6.0/benchmark.sh golang-github-pelletier-go-toml-1.8.1/benchmark.sh --- golang-github-pelletier-go-toml-1.6.0/benchmark.sh 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/benchmark.sh 2020-09-12 18:42:04.000000000 +0000 @@ -20,11 +20,15 @@ pushd ${ref_tempdir} >/dev/null git checkout ${reference_ref} >/dev/null 2>/dev/null go test -bench=. -benchmem | tee ${ref_benchmark} +cd benchmark +go test -bench=. -benchmem | tee -a ${ref_benchmark} popd >/dev/null echo "" echo "=== local" go test -bench=. -benchmem | tee ${local_benchmark} +cd benchmark +go test -bench=. -benchmem | tee -a ${local_benchmark} echo "" echo "=== diff" diff -Nru golang-github-pelletier-go-toml-1.6.0/benchmark_test.go golang-github-pelletier-go-toml-1.8.1/benchmark_test.go --- golang-github-pelletier-go-toml-1.6.0/benchmark_test.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/benchmark_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,192 +0,0 @@ -package toml - -import ( - "bytes" - "encoding/json" - "io/ioutil" - "testing" - "time" - - burntsushi "github.com/BurntSushi/toml" - yaml "gopkg.in/yaml.v2" -) - -type benchmarkDoc struct { - Table struct { - Key string - Subtable struct { - Key string - } - Inline struct { - Name struct { - First string - Last string - } - Point struct { - X int64 - U int64 - } - } - } - String struct { - Basic struct { - Basic string - } - Multiline struct { - Key1 string - Key2 string - Key3 string - Continued struct { - Key1 string - Key2 string - Key3 string - } - } - Literal struct { - Winpath string - Winpath2 string - Quoted string - Regex string - Multiline struct { - Regex2 string - Lines string - } - } - } - Integer struct { - Key1 int64 - Key2 int64 - Key3 int64 - Key4 int64 - Underscores struct { - Key1 int64 - Key2 int64 - Key3 int64 - } - } - Float struct { - Fractional struct { - Key1 float64 - Key2 float64 - Key3 float64 - } - Exponent struct { - Key1 float64 - Key2 float64 - Key3 float64 - } - Both struct { - Key float64 - } - Underscores struct { - Key1 float64 - Key2 float64 - } - } - Boolean struct { - True bool - False bool - } - Datetime struct { - Key1 time.Time - Key2 time.Time - Key3 time.Time - } - Array struct { - Key1 []int64 - Key2 []string - Key3 [][]int64 - // TODO: Key4 not supported by go-toml's Unmarshal - Key5 []int64 - Key6 []int64 - } - Products []struct { - Name string - Sku int64 - Color string - } - Fruit []struct { - Name string - Physical struct { - Color string - Shape string - Variety []struct { - Name string - } - } - } -} - -func BenchmarkParseToml(b *testing.B) { - fileBytes, err := ioutil.ReadFile("benchmark.toml") - if err != nil { - b.Fatal(err) - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, err := LoadReader(bytes.NewReader(fileBytes)) - if err != nil { - b.Fatal(err) - } - } -} - -func BenchmarkUnmarshalToml(b *testing.B) { - bytes, err := ioutil.ReadFile("benchmark.toml") - if err != nil { - b.Fatal(err) - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - target := benchmarkDoc{} - err := Unmarshal(bytes, &target) - if err != nil { - b.Fatal(err) - } - } -} - -func BenchmarkUnmarshalBurntSushiToml(b *testing.B) { - bytes, err := ioutil.ReadFile("benchmark.toml") - if err != nil { - b.Fatal(err) - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - target := benchmarkDoc{} - err := burntsushi.Unmarshal(bytes, &target) - if err != nil { - b.Fatal(err) - } - } -} - -func BenchmarkUnmarshalJson(b *testing.B) { - bytes, err := ioutil.ReadFile("benchmark.json") - if err != nil { - b.Fatal(err) - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - target := benchmarkDoc{} - err := json.Unmarshal(bytes, &target) - if err != nil { - b.Fatal(err) - } - } -} - -func BenchmarkUnmarshalYaml(b *testing.B) { - bytes, err := ioutil.ReadFile("benchmark.yml") - if err != nil { - b.Fatal(err) - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - target := benchmarkDoc{} - err := yaml.Unmarshal(bytes, &target) - if err != nil { - b.Fatal(err) - } - } -} diff -Nru golang-github-pelletier-go-toml-1.6.0/benchmark.toml golang-github-pelletier-go-toml-1.8.1/benchmark.toml --- golang-github-pelletier-go-toml-1.6.0/benchmark.toml 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/benchmark.toml 1970-01-01 00:00:00.000000000 +0000 @@ -1,244 +0,0 @@ -################################################################################ -## Comment - -# Speak your mind with the hash symbol. They go from the symbol to the end of -# the line. - - -################################################################################ -## Table - -# Tables (also known as hash tables or dictionaries) are collections of -# key/value pairs. They appear in square brackets on a line by themselves. - -[table] - -key = "value" # Yeah, you can do this. - -# Nested tables are denoted by table names with dots in them. Name your tables -# whatever crap you please, just don't use #, ., [ or ]. - -[table.subtable] - -key = "another value" - -# You don't need to specify all the super-tables if you don't want to. TOML -# knows how to do it for you. - -# [x] you -# [x.y] don't -# [x.y.z] need these -[x.y.z.w] # for this to work - - -################################################################################ -## Inline Table - -# Inline tables provide a more compact syntax for expressing tables. They are -# especially useful for grouped data that can otherwise quickly become verbose. -# Inline tables are enclosed in curly braces `{` and `}`. No newlines are -# allowed between the curly braces unless they are valid within a value. - -[table.inline] - -name = { first = "Tom", last = "Preston-Werner" } -point = { x = 1, y = 2 } - - -################################################################################ -## String - -# There are four ways to express strings: basic, multi-line basic, literal, and -# multi-line literal. All strings must contain only valid UTF-8 characters. - -[string.basic] - -basic = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF." - -[string.multiline] - -# The following strings are byte-for-byte equivalent: -key1 = "One\nTwo" -key2 = """One\nTwo""" -key3 = """ -One -Two""" - -[string.multiline.continued] - -# The following strings are byte-for-byte equivalent: -key1 = "The quick brown fox jumps over the lazy dog." - -key2 = """ -The quick brown \ - - - fox jumps over \ - the lazy dog.""" - -key3 = """\ - The quick brown \ - fox jumps over \ - the lazy dog.\ - """ - -[string.literal] - -# What you see is what you get. -winpath = 'C:\Users\nodejs\templates' -winpath2 = '\\ServerX\admin$\system32\' -quoted = 'Tom "Dubs" Preston-Werner' -regex = '<\i\c*\s*>' - - -[string.literal.multiline] - -regex2 = '''I [dw]on't need \d{2} apples''' -lines = ''' -The first newline is -trimmed in raw strings. - All other whitespace - is preserved. -''' - - -################################################################################ -## Integer - -# Integers are whole numbers. Positive numbers may be prefixed with a plus sign. -# Negative numbers are prefixed with a minus sign. - -[integer] - -key1 = +99 -key2 = 42 -key3 = 0 -key4 = -17 - -[integer.underscores] - -# For large numbers, you may use underscores to enhance readability. Each -# underscore must be surrounded by at least one digit. -key1 = 1_000 -key2 = 5_349_221 -key3 = 1_2_3_4_5 # valid but inadvisable - - -################################################################################ -## Float - -# A float consists of an integer part (which may be prefixed with a plus or -# minus sign) followed by a fractional part and/or an exponent part. - -[float.fractional] - -key1 = +1.0 -key2 = 3.1415 -key3 = -0.01 - -[float.exponent] - -key1 = 5e+22 -key2 = 1e6 -key3 = -2E-2 - -[float.both] - -key = 6.626e-34 - -[float.underscores] - -key1 = 9_224_617.445_991_228_313 -key2 = 1e1_00 - - -################################################################################ -## Boolean - -# Booleans are just the tokens you're used to. Always lowercase. - -[boolean] - -True = true -False = false - - -################################################################################ -## Datetime - -# Datetimes are RFC 3339 dates. - -[datetime] - -key1 = 1979-05-27T07:32:00Z -key2 = 1979-05-27T00:32:00-07:00 -key3 = 1979-05-27T00:32:00.999999-07:00 - - -################################################################################ -## Array - -# Arrays are square brackets with other primitives inside. Whitespace is -# ignored. Elements are separated by commas. Data types may not be mixed. - -[array] - -key1 = [ 1, 2, 3 ] -key2 = [ "red", "yellow", "green" ] -key3 = [ [ 1, 2 ], [3, 4, 5] ] -#key4 = [ [ 1, 2 ], ["a", "b", "c"] ] # this is ok - -# Arrays can also be multiline. So in addition to ignoring whitespace, arrays -# also ignore newlines between the brackets. Terminating commas are ok before -# the closing bracket. - -key5 = [ - 1, 2, 3 -] -key6 = [ - 1, - 2, # this is ok -] - - -################################################################################ -## Array of Tables - -# These can be expressed by using a table name in double brackets. Each table -# with the same double bracketed name will be an element in the array. The -# tables are inserted in the order encountered. - -[[products]] - -name = "Hammer" -sku = 738594937 - -[[products]] - -[[products]] - -name = "Nail" -sku = 284758393 -color = "gray" - - -# You can create nested arrays of tables as well. - -[[fruit]] - name = "apple" - - [fruit.physical] - color = "red" - shape = "round" - - [[fruit.variety]] - name = "red delicious" - - [[fruit.variety]] - name = "granny smith" - -[[fruit]] - name = "banana" - - [[fruit.variety]] - name = "plantain" diff -Nru golang-github-pelletier-go-toml-1.6.0/benchmark.yml golang-github-pelletier-go-toml-1.8.1/benchmark.yml --- golang-github-pelletier-go-toml-1.6.0/benchmark.yml 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/benchmark.yml 1970-01-01 00:00:00.000000000 +0000 @@ -1,121 +0,0 @@ ---- -array: - key1: - - 1 - - 2 - - 3 - key2: - - red - - yellow - - green - key3: - - - 1 - - 2 - - - 3 - - 4 - - 5 - key4: - - - 1 - - 2 - - - a - - b - - c - key5: - - 1 - - 2 - - 3 - key6: - - 1 - - 2 -boolean: - 'False': false - 'True': true -datetime: - key1: '1979-05-27T07:32:00Z' - key2: '1979-05-27T00:32:00-07:00' - key3: '1979-05-27T00:32:00.999999-07:00' -float: - both: - key: 6.626e-34 - exponent: - key1: 5.0e+22 - key2: 1000000 - key3: -0.02 - fractional: - key1: 1 - key2: 3.1415 - key3: -0.01 - underscores: - key1: 9224617.445991227 - key2: 1.0e+100 -fruit: -- name: apple - physical: - color: red - shape: round - variety: - - name: red delicious - - name: granny smith -- name: banana - variety: - - name: plantain -integer: - key1: 99 - key2: 42 - key3: 0 - key4: -17 - underscores: - key1: 1000 - key2: 5349221 - key3: 12345 -products: -- name: Hammer - sku: 738594937 -- {} -- color: gray - name: Nail - sku: 284758393 -string: - basic: - basic: "I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF." - literal: - multiline: - lines: | - The first newline is - trimmed in raw strings. - All other whitespace - is preserved. - regex2: I [dw]on't need \d{2} apples - quoted: Tom "Dubs" Preston-Werner - regex: "<\\i\\c*\\s*>" - winpath: C:\Users\nodejs\templates - winpath2: "\\\\ServerX\\admin$\\system32\\" - multiline: - continued: - key1: The quick brown fox jumps over the lazy dog. - key2: The quick brown fox jumps over the lazy dog. - key3: The quick brown fox jumps over the lazy dog. - key1: |- - One - Two - key2: |- - One - Two - key3: |- - One - Two -table: - inline: - name: - first: Tom - last: Preston-Werner - point: - x: 1 - y: 2 - key: value - subtable: - key: another value -x: - y: - z: - w: {} diff -Nru golang-github-pelletier-go-toml-1.6.0/debian/changelog golang-github-pelletier-go-toml-1.8.1/debian/changelog --- golang-github-pelletier-go-toml-1.6.0/debian/changelog 2020-09-22 08:51:57.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/debian/changelog 2020-10-24 01:36:11.000000000 +0000 @@ -1,8 +1,16 @@ -golang-github-pelletier-go-toml (1.6.0-1build1) groovy; urgency=medium +golang-github-pelletier-go-toml (1.8.1-1) unstable; urgency=medium - * No-change rebuild using new golang + * New upstream version 1.8.1 + * Bump debhelper dependency to "Build-Depends: debhelper-compat (= 13)" + * debian/control: Update dependencies as per go.mod and benchmark/go.mod + * Update package description according to README.md + * debian/rules: Exclude cmd/tomltestgen which is used by upstream only + for generating toml_testgen_test.go + * debian/golang-github-pelletier-go-toml.install: Install the entire + usr/bin to ensure /usr/bin/jsontoml is included + * Mention the new jsontoml command-line program in package description - -- Steve Langasek Tue, 22 Sep 2020 08:51:57 +0000 + -- Anthony Fok Fri, 23 Oct 2020 19:36:11 -0600 golang-github-pelletier-go-toml (1.6.0-1) unstable; urgency=medium diff -Nru golang-github-pelletier-go-toml-1.6.0/debian/control golang-github-pelletier-go-toml-1.8.1/debian/control --- golang-github-pelletier-go-toml-1.6.0/debian/control 2020-09-22 08:51:57.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/debian/control 2020-10-24 01:35:55.000000000 +0000 @@ -1,17 +1,16 @@ Source: golang-github-pelletier-go-toml -Maintainer: Ubuntu Developers -XSBC-Original-Maintainer: Debian Go Packaging Team +Maintainer: Debian Go Packaging Team Uploaders: Dr. Tobias Quathamer , Anthony Fok Section: devel Testsuite: autopkgtest-pkg-go Priority: optional -Build-Depends: debhelper-compat (= 12), +Build-Depends: debhelper-compat (= 13), dh-golang, golang-any, - golang-github-burntsushi-toml-dev, - golang-github-davecgh-go-spew-dev, - golang-gopkg-yaml.v2-dev (>= 2.2.4~) + golang-github-burntsushi-toml-dev (>= 0.3.1), + golang-github-davecgh-go-spew-dev (>= 1.1.1), + golang-gopkg-yaml.v2-dev (>= 2.3.0) Standards-Version: 4.5.0 Vcs-Browser: https://salsa.debian.org/go-team/packages/golang-github-pelletier-go-toml Vcs-Git: https://salsa.debian.org/go-team/packages/golang-github-pelletier-go-toml.git @@ -23,33 +22,41 @@ Architecture: all Depends: ${shlibs:Depends}, ${misc:Depends}, - golang-github-burntsushi-toml-dev, - golang-github-davecgh-go-spew-dev, - golang-gopkg-yaml.v2-dev (>= 2.2.4~) -Description: Go library for the TOML language + golang-github-burntsushi-toml-dev (>= 0.3.1), + golang-github-davecgh-go-spew-dev (>= 1.1.1), + golang-gopkg-yaml.v2-dev (>= 2.3.0) +Description: Go library for the TOML format This library supports TOML (Tom's Obvious, Minimal Language) - version 0.5.0. + version v1.0.0-rc.1. . - TOML aims to be a minimal configuration file format that's - easy to read due to obvious semantics. TOML is designed - to map unambiguously to a hash table. TOML should be - easy to parse into data structures in a wide variety - of languages. + Go-toml provides the following features for using data parsed + from TOML documents: + . + * Load TOML documents from files and string data + * Easily navigate TOML structure using Tree + * Marshaling and unmarshaling to and from data structures + * Line & column position data for all parsed elements + * [Query support similar to JSON-Path](query/) + * Syntax errors contain line and column numbers Package: golang-github-pelletier-go-toml Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends} Built-Using: ${misc:Built-Using} -Description: Go library for the TOML language -- commandline programs +Description: Go library for the TOML format — command-line programs This library supports TOML (Tom's Obvious, Minimal Language) - version 0.5.0. + version v1.0.0-rc.1. + . + Go-toml provides the following features for using data parsed + from TOML documents: . - TOML aims to be a minimal configuration file format that's - easy to read due to obvious semantics. TOML is designed - to map unambiguously to a hash table. TOML should be - easy to parse into data structures in a wide variety - of languages. + * Load TOML documents from files and string data + * Easily navigate TOML structure using Tree + * Marshaling and unmarshaling to and from data structures + * Line & column position data for all parsed elements + * [Query support similar to JSON-Path](query/) + * Syntax errors contain line and column numbers . - This package contains the two commandline programs tomll - and tomljson. + This package contains the three command-line programs + tomll, tomljson and jsontoml. diff -Nru golang-github-pelletier-go-toml-1.6.0/debian/golang-github-pelletier-go-toml.install golang-github-pelletier-go-toml-1.8.1/debian/golang-github-pelletier-go-toml.install --- golang-github-pelletier-go-toml-1.6.0/debian/golang-github-pelletier-go-toml.install 2017-07-29 05:07:34.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/debian/golang-github-pelletier-go-toml.install 2020-10-24 01:31:38.000000000 +0000 @@ -1 +1 @@ -usr/bin/toml* +usr/bin diff -Nru golang-github-pelletier-go-toml-1.6.0/debian/rules golang-github-pelletier-go-toml-1.8.1/debian/rules --- golang-github-pelletier-go-toml-1.6.0/debian/rules 2019-08-11 09:49:41.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/debian/rules 2020-10-24 01:31:27.000000000 +0000 @@ -3,5 +3,8 @@ # Include test fixtures export DH_GOLANG_INSTALL_EXTRA := $(wildcard *.toml) +# Exclude toml_testgen_test.go generation tool +export DH_GOLANG_EXCLUDES := cmd/tomltestgen + %: dh $@ --buildsystem=golang --with=golang diff -Nru golang-github-pelletier-go-toml-1.6.0/doc_test.go golang-github-pelletier-go-toml-1.8.1/doc_test.go --- golang-github-pelletier-go-toml-1.6.0/doc_test.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/doc_test.go 2020-09-12 18:42:04.000000000 +0000 @@ -5,6 +5,7 @@ import ( "fmt" "log" + "os" toml "github.com/pelletier/go-toml" ) @@ -104,3 +105,66 @@ // Output: // user= pelletier } + +func ExampleEncoder_anonymous() { + type Credentials struct { + User string `toml:"user"` + Password string `toml:"password"` + } + + type Protocol struct { + Name string `toml:"name"` + } + + type Config struct { + Version int `toml:"version"` + Credentials + Protocol `toml:"Protocol"` + } + config := Config{ + Version: 2, + Credentials: Credentials{ + User: "pelletier", + Password: "mypassword", + }, + Protocol: Protocol{ + Name: "tcp", + }, + } + fmt.Println("Default:") + fmt.Println("---------------") + + def := toml.NewEncoder(os.Stdout) + if err := def.Encode(config); err != nil { + log.Fatal(err) + } + + fmt.Println("---------------") + fmt.Println("With promotion:") + fmt.Println("---------------") + + prom := toml.NewEncoder(os.Stdout).PromoteAnonymous(true) + if err := prom.Encode(config); err != nil { + log.Fatal(err) + } + // Output: + // Default: + // --------------- + // password = "mypassword" + // user = "pelletier" + // version = 2 + // + // [Protocol] + // name = "tcp" + // --------------- + // With promotion: + // --------------- + // version = 2 + // + // [Credentials] + // password = "mypassword" + // user = "pelletier" + // + // [Protocol] + // name = "tcp" +} diff -Nru golang-github-pelletier-go-toml-1.6.0/example-crlf.toml golang-github-pelletier-go-toml-1.8.1/example-crlf.toml --- golang-github-pelletier-go-toml-1.6.0/example-crlf.toml 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/example-crlf.toml 2020-09-12 18:42:04.000000000 +0000 @@ -27,3 +27,4 @@ [clients] data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it +score = 4e-08 # to make sure leading zeroes in exponent parts of floats are supported \ No newline at end of file diff -Nru golang-github-pelletier-go-toml-1.6.0/example.toml golang-github-pelletier-go-toml-1.8.1/example.toml --- golang-github-pelletier-go-toml-1.6.0/example.toml 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/example.toml 2020-09-12 18:42:04.000000000 +0000 @@ -27,3 +27,4 @@ [clients] data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it +score = 4e-08 # to make sure leading zeroes in exponent parts of floats are supported \ No newline at end of file diff -Nru golang-github-pelletier-go-toml-1.6.0/go.mod golang-github-pelletier-go-toml-1.8.1/go.mod --- golang-github-pelletier-go-toml-1.6.0/go.mod 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/go.mod 2020-09-12 18:42:04.000000000 +0000 @@ -2,8 +2,4 @@ go 1.12 -require ( - github.com/BurntSushi/toml v0.3.1 - github.com/davecgh/go-spew v1.1.1 - gopkg.in/yaml.v2 v2.2.4 -) +require github.com/davecgh/go-spew v1.1.1 diff -Nru golang-github-pelletier-go-toml-1.6.0/go.sum golang-github-pelletier-go-toml-1.8.1/go.sum --- golang-github-pelletier-go-toml-1.6.0/go.sum 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/go.sum 2020-09-12 18:42:04.000000000 +0000 @@ -9,3 +9,11 @@ gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff -Nru golang-github-pelletier-go-toml-1.6.0/keysparsing.go golang-github-pelletier-go-toml-1.8.1/keysparsing.go --- golang-github-pelletier-go-toml-1.6.0/keysparsing.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/keysparsing.go 2020-09-12 18:42:04.000000000 +0000 @@ -5,7 +5,6 @@ import ( "errors" "fmt" - "unicode" ) // Convert the bare key group string to an array. @@ -109,5 +108,5 @@ } func isValidBareChar(r rune) bool { - return isAlphanumeric(r) || r == '-' || unicode.IsNumber(r) + return isAlphanumeric(r) || r == '-' || isDigit(r) } diff -Nru golang-github-pelletier-go-toml-1.6.0/lexer.go golang-github-pelletier-go-toml-1.8.1/lexer.go --- golang-github-pelletier-go-toml-1.6.0/lexer.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/lexer.go 2020-09-12 18:42:04.000000000 +0000 @@ -26,7 +26,7 @@ currentTokenStart int currentTokenStop int tokens []token - depth int + brackets []rune line int col int endbufferLine int @@ -123,6 +123,8 @@ for { next := l.peek() switch next { + case '}': // after '{' + return l.lexRightCurlyBrace case '[': return l.lexTableKey case '#': @@ -140,10 +142,6 @@ l.skip() } - if l.depth > 0 { - return l.lexRvalue - } - if isKeyStartChar(next) { return l.lexKey } @@ -167,10 +165,8 @@ case '=': return l.lexEqual case '[': - l.depth++ return l.lexLeftBracket case ']': - l.depth-- return l.lexRightBracket case '{': return l.lexLeftCurlyBrace @@ -188,12 +184,10 @@ fallthrough case '\n': l.skip() - if l.depth == 0 { - return l.lexVoid + if len(l.brackets) > 0 && l.brackets[len(l.brackets)-1] == '[' { + return l.lexRvalue } - return l.lexRvalue - case '_': - return l.errorf("cannot start number with underscore") + return l.lexVoid } if l.follow("true") { @@ -236,10 +230,6 @@ return l.lexNumber } - if isAlphanumeric(next) { - return l.lexKey - } - return l.errorf("no value can start with %c", next) } @@ -250,13 +240,18 @@ func (l *tomlLexer) lexLeftCurlyBrace() tomlLexStateFn { l.next() l.emit(tokenLeftCurlyBrace) + l.brackets = append(l.brackets, '{') return l.lexVoid } func (l *tomlLexer) lexRightCurlyBrace() tomlLexStateFn { l.next() l.emit(tokenRightCurlyBrace) - return l.lexVoid + if len(l.brackets) == 0 || l.brackets[len(l.brackets)-1] != '{' { + return l.errorf("cannot have '}' here") + } + l.brackets = l.brackets[:len(l.brackets)-1] + return l.lexRvalue } func (l *tomlLexer) lexDate() tomlLexStateFn { @@ -302,13 +297,16 @@ func (l *tomlLexer) lexComma() tomlLexStateFn { l.next() l.emit(tokenComma) + if len(l.brackets) > 0 && l.brackets[len(l.brackets)-1] == '{' { + return l.lexVoid + } return l.lexRvalue } // Parse the key and emits its value without escape sequences. // bare keys, basic string keys and literal string keys are supported. func (l *tomlLexer) lexKey() tomlLexStateFn { - growingString := "" + var sb strings.Builder for r := l.peek(); isKeyChar(r) || r == '\n' || r == '\r'; r = l.peek() { if r == '"' { @@ -317,7 +315,9 @@ if err != nil { return l.errorf(err.Error()) } - growingString += "\"" + str + "\"" + sb.WriteString("\"") + sb.WriteString(str) + sb.WriteString("\"") l.next() continue } else if r == '\'' { @@ -326,22 +326,45 @@ if err != nil { return l.errorf(err.Error()) } - growingString += "'" + str + "'" + sb.WriteString("'") + sb.WriteString(str) + sb.WriteString("'") l.next() continue } else if r == '\n' { return l.errorf("keys cannot contain new lines") } else if isSpace(r) { - break + var str strings.Builder + str.WriteString(" ") + + // skip trailing whitespace + l.next() + for r = l.peek(); isSpace(r); r = l.peek() { + str.WriteRune(r) + l.next() + } + // break loop if not a dot + if r != '.' { + break + } + str.WriteString(".") + // skip trailing whitespace after dot + l.next() + for r = l.peek(); isSpace(r); r = l.peek() { + str.WriteRune(r) + l.next() + } + sb.WriteString(str.String()) + continue } else if r == '.' { // skip } else if !isValidBareChar(r) { return l.errorf("keys cannot contain %c character", r) } - growingString += string(r) + sb.WriteRune(r) l.next() } - l.emitWithValue(tokenKey, growingString) + l.emitWithValue(tokenKey, sb.String()) return l.lexVoid } @@ -361,11 +384,12 @@ func (l *tomlLexer) lexLeftBracket() tomlLexStateFn { l.next() l.emit(tokenLeftBracket) + l.brackets = append(l.brackets, '[') return l.lexRvalue } func (l *tomlLexer) lexLiteralStringAsString(terminator string, discardLeadingNewLine bool) (string, error) { - growingString := "" + var sb strings.Builder if discardLeadingNewLine { if l.follow("\r\n") { @@ -379,14 +403,14 @@ // find end of string for { if l.follow(terminator) { - return growingString, nil + return sb.String(), nil } next := l.peek() if next == eof { break } - growingString += string(l.next()) + sb.WriteRune(l.next()) } return "", errors.New("unclosed string") @@ -420,7 +444,7 @@ // Terminator is the substring indicating the end of the token. // The resulting string does not include the terminator. func (l *tomlLexer) lexStringAsString(terminator string, discardLeadingNewLine, acceptNewLines bool) (string, error) { - growingString := "" + var sb strings.Builder if discardLeadingNewLine { if l.follow("\r\n") { @@ -433,7 +457,7 @@ for { if l.follow(terminator) { - return growingString, nil + return sb.String(), nil } if l.follow("\\") { @@ -451,72 +475,72 @@ l.next() } case '"': - growingString += "\"" + sb.WriteString("\"") l.next() case 'n': - growingString += "\n" + sb.WriteString("\n") l.next() case 'b': - growingString += "\b" + sb.WriteString("\b") l.next() case 'f': - growingString += "\f" + sb.WriteString("\f") l.next() case '/': - growingString += "/" + sb.WriteString("/") l.next() case 't': - growingString += "\t" + sb.WriteString("\t") l.next() case 'r': - growingString += "\r" + sb.WriteString("\r") l.next() case '\\': - growingString += "\\" + sb.WriteString("\\") l.next() case 'u': l.next() - code := "" + var code strings.Builder for i := 0; i < 4; i++ { c := l.peek() if !isHexDigit(c) { return "", errors.New("unfinished unicode escape") } l.next() - code = code + string(c) + code.WriteRune(c) } - intcode, err := strconv.ParseInt(code, 16, 32) + intcode, err := strconv.ParseInt(code.String(), 16, 32) if err != nil { - return "", errors.New("invalid unicode escape: \\u" + code) + return "", errors.New("invalid unicode escape: \\u" + code.String()) } - growingString += string(rune(intcode)) + sb.WriteRune(rune(intcode)) case 'U': l.next() - code := "" + var code strings.Builder for i := 0; i < 8; i++ { c := l.peek() if !isHexDigit(c) { return "", errors.New("unfinished unicode escape") } l.next() - code = code + string(c) + code.WriteRune(c) } - intcode, err := strconv.ParseInt(code, 16, 64) + intcode, err := strconv.ParseInt(code.String(), 16, 64) if err != nil { - return "", errors.New("invalid unicode escape: \\U" + code) + return "", errors.New("invalid unicode escape: \\U" + code.String()) } - growingString += string(rune(intcode)) + sb.WriteRune(rune(intcode)) default: return "", errors.New("invalid escape sequence: \\" + string(l.peek())) } } else { r := l.peek() - if 0x00 <= r && r <= 0x1F && !(acceptNewLines && (r == '\n' || r == '\r')) { + if 0x00 <= r && r <= 0x1F && r != '\t' && !(acceptNewLines && (r == '\n' || r == '\r')) { return "", fmt.Errorf("unescaped control character %U", r) } l.next() - growingString += string(r) + sb.WriteRune(r) } if l.peek() == eof { @@ -543,7 +567,6 @@ } str, err := l.lexStringAsString(terminator, discardLeadingNewLine, acceptNewLines) - if err != nil { return l.errorf(err.Error()) } @@ -615,6 +638,10 @@ func (l *tomlLexer) lexRightBracket() tomlLexStateFn { l.next() l.emit(tokenRightBracket) + if len(l.brackets) == 0 || l.brackets[len(l.brackets)-1] != '[' { + return l.errorf("cannot have ']' here") + } + l.brackets = l.brackets[:len(l.brackets)-1] return l.lexRvalue } @@ -748,19 +775,19 @@ // /!\ also matches the empty string // // Example matches: - //1979-05-27T07:32:00Z - //1979-05-27T00:32:00-07:00 - //1979-05-27T00:32:00.999999-07:00 - //1979-05-27 07:32:00Z - //1979-05-27 00:32:00-07:00 - //1979-05-27 00:32:00.999999-07:00 - //1979-05-27T07:32:00 - //1979-05-27T00:32:00.999999 - //1979-05-27 07:32:00 - //1979-05-27 00:32:00.999999 - //1979-05-27 - //07:32:00 - //00:32:00.999999 + // 1979-05-27T07:32:00Z + // 1979-05-27T00:32:00-07:00 + // 1979-05-27T00:32:00.999999-07:00 + // 1979-05-27 07:32:00Z + // 1979-05-27 00:32:00-07:00 + // 1979-05-27 00:32:00.999999-07:00 + // 1979-05-27T07:32:00 + // 1979-05-27T00:32:00.999999 + // 1979-05-27 07:32:00 + // 1979-05-27 00:32:00.999999 + // 1979-05-27 + // 07:32:00 + // 00:32:00.999999 dateRegexp = regexp.MustCompile(`^(?:\d{1,4}-\d{2}-\d{2})?(?:[T ]?\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})?)?`) } diff -Nru golang-github-pelletier-go-toml-1.6.0/lexer_test.go golang-github-pelletier-go-toml-1.8.1/lexer_test.go --- golang-github-pelletier-go-toml-1.6.0/lexer_test.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/lexer_test.go 2020-09-12 18:42:04.000000000 +0000 @@ -8,7 +8,7 @@ func testFlow(t *testing.T, input string, expectedFlow []token) { tokens := lexToml([]byte(input)) if !reflect.DeepEqual(tokens, expectedFlow) { - t.Fatal("Different flows. Expected\n", expectedFlow, "\nGot:\n", tokens) + t.Fatalf("Different flows.\nExpected:\n%v\nGot:\n%v", expectedFlow, tokens) } } @@ -22,11 +22,20 @@ } func TestNestedQuotedUnicodeKeyGroup(t *testing.T) { - testFlow(t, `[ j . "ʞ" . l ]`, []token{ + testFlow(t, `[ j . "ʞ" . l . 'ɯ' ]`, []token{ {Position{1, 1}, tokenLeftBracket, "["}, - {Position{1, 2}, tokenKeyGroup, ` j . "ʞ" . l `}, - {Position{1, 15}, tokenRightBracket, "]"}, - {Position{1, 16}, tokenEOF, ""}, + {Position{1, 2}, tokenKeyGroup, ` j . "ʞ" . l . 'ɯ' `}, + {Position{1, 21}, tokenRightBracket, "]"}, + {Position{1, 22}, tokenEOF, ""}, + }) +} + +func TestNestedQuotedUnicodeKeyAssign(t *testing.T) { + testFlow(t, ` j . "ʞ" . l . 'ɯ' = 3`, []token{ + {Position{1, 2}, tokenKey, `j . "ʞ" . l . 'ɯ'`}, + {Position{1, 20}, tokenEqual, "="}, + {Position{1, 22}, tokenInteger, "3"}, + {Position{1, 23}, tokenEOF, ""}, }) } @@ -105,9 +114,9 @@ } func TestBasicKeyWithInternationalCharacters(t *testing.T) { - testFlow(t, "héllÖ", []token{ - {Position{1, 1}, tokenKey, "héllÖ"}, - {Position{1, 6}, tokenEOF, ""}, + testFlow(t, "'héllÖ'", []token{ + {Position{1, 1}, tokenKey, "'héllÖ'"}, + {Position{1, 8}, tokenEOF, ""}, }) } @@ -654,6 +663,13 @@ {Position{6, 9}, tokenEOF, ""}, }) + testFlow(t, `foo = """hello world"""`, []token{ + {Position{1, 1}, tokenKey, "foo"}, + {Position{1, 5}, tokenEqual, "="}, + {Position{1, 10}, tokenString, "hello\tworld"}, + {Position{1, 24}, tokenEOF, ""}, + }) + testFlow(t, "key2 = \"\"\"\nThe quick brown \\\n\n\n fox jumps over \\\n the lazy dog.\"\"\"", []token{ {Position{1, 1}, tokenKey, "key2"}, {Position{1, 6}, tokenEqual, "="}, @@ -691,6 +707,7 @@ {Position{1, 22}, tokenEOF, ""}, }) } + func TestEscapeInString(t *testing.T) { testFlow(t, `foo = "\b\f\/"`, []token{ {Position{1, 1}, tokenKey, "foo"}, @@ -700,6 +717,15 @@ }) } +func TestTabInString(t *testing.T) { + testFlow(t, `foo = "hello world"`, []token{ + {Position{1, 1}, tokenKey, "foo"}, + {Position{1, 5}, tokenEqual, "="}, + {Position{1, 8}, tokenString, "hello\tworld"}, + {Position{1, 20}, tokenEOF, ""}, + }) +} + func TestKeyGroupArray(t *testing.T) { testFlow(t, "[[foo]]", []token{ {Position{1, 1}, tokenDoubleLeftBracket, "[["}, @@ -718,6 +744,15 @@ }) } +func TestQuotedKeyTab(t *testing.T) { + testFlow(t, "\"num\tber\" = 123", []token{ + {Position{1, 1}, tokenKey, "\"num\tber\""}, + {Position{1, 11}, tokenEqual, "="}, + {Position{1, 13}, tokenInteger, "123"}, + {Position{1, 16}, tokenEOF, ""}, + }) +} + func TestKeyNewline(t *testing.T) { testFlow(t, "a\n= 4", []token{ {Position{1, 1}, tokenError, "keys cannot contain new lines"}, @@ -747,6 +782,16 @@ }) } +func TestLexInlineTableEmpty(t *testing.T) { + testFlow(t, `foo = {}`, []token{ + {Position{1, 1}, tokenKey, "foo"}, + {Position{1, 5}, tokenEqual, "="}, + {Position{1, 7}, tokenLeftCurlyBrace, "{"}, + {Position{1, 8}, tokenRightCurlyBrace, "}"}, + {Position{1, 9}, tokenEOF, ""}, + }) +} + func TestLexInlineTableBareKey(t *testing.T) { testFlow(t, `foo = { bar = "baz" }`, []token{ {Position{1, 1}, tokenKey, "foo"}, @@ -773,6 +818,116 @@ }) } +func TestLexInlineTableBareKeyInArray(t *testing.T) { + testFlow(t, `foo = [{ -bar_ = "baz" }]`, []token{ + {Position{1, 1}, tokenKey, "foo"}, + {Position{1, 5}, tokenEqual, "="}, + {Position{1, 7}, tokenLeftBracket, "["}, + {Position{1, 8}, tokenLeftCurlyBrace, "{"}, + {Position{1, 10}, tokenKey, "-bar_"}, + {Position{1, 16}, tokenEqual, "="}, + {Position{1, 19}, tokenString, "baz"}, + {Position{1, 24}, tokenRightCurlyBrace, "}"}, + {Position{1, 25}, tokenRightBracket, "]"}, + {Position{1, 26}, tokenEOF, ""}, + }) +} + +func TestLexInlineTableError1(t *testing.T) { + testFlow(t, `foo = { 123 = 0 ]`, []token{ + {Position{1, 1}, tokenKey, "foo"}, + {Position{1, 5}, tokenEqual, "="}, + {Position{1, 7}, tokenLeftCurlyBrace, "{"}, + {Position{1, 9}, tokenKey, "123"}, + {Position{1, 13}, tokenEqual, "="}, + {Position{1, 15}, tokenInteger, "0"}, + {Position{1, 17}, tokenRightBracket, "]"}, + {Position{1, 18}, tokenError, "cannot have ']' here"}, + }) +} + +func TestLexInlineTableError2(t *testing.T) { + testFlow(t, `foo = { 123 = 0 }}`, []token{ + {Position{1, 1}, tokenKey, "foo"}, + {Position{1, 5}, tokenEqual, "="}, + {Position{1, 7}, tokenLeftCurlyBrace, "{"}, + {Position{1, 9}, tokenKey, "123"}, + {Position{1, 13}, tokenEqual, "="}, + {Position{1, 15}, tokenInteger, "0"}, + {Position{1, 17}, tokenRightCurlyBrace, "}"}, + {Position{1, 18}, tokenRightCurlyBrace, "}"}, + {Position{1, 19}, tokenError, "cannot have '}' here"}, + }) +} + +func TestLexInlineTableDottedKey1(t *testing.T) { + testFlow(t, `foo = { a = 0, 123.45abc = 0 }`, []token{ + {Position{1, 1}, tokenKey, "foo"}, + {Position{1, 5}, tokenEqual, "="}, + {Position{1, 7}, tokenLeftCurlyBrace, "{"}, + {Position{1, 9}, tokenKey, "a"}, + {Position{1, 11}, tokenEqual, "="}, + {Position{1, 13}, tokenInteger, "0"}, + {Position{1, 14}, tokenComma, ","}, + {Position{1, 16}, tokenKey, "123.45abc"}, + {Position{1, 26}, tokenEqual, "="}, + {Position{1, 28}, tokenInteger, "0"}, + {Position{1, 30}, tokenRightCurlyBrace, "}"}, + {Position{1, 31}, tokenEOF, ""}, + }) +} + +func TestLexInlineTableDottedKey2(t *testing.T) { + testFlow(t, `foo = { a = 0, '123'.'45abc' = 0 }`, []token{ + {Position{1, 1}, tokenKey, "foo"}, + {Position{1, 5}, tokenEqual, "="}, + {Position{1, 7}, tokenLeftCurlyBrace, "{"}, + {Position{1, 9}, tokenKey, "a"}, + {Position{1, 11}, tokenEqual, "="}, + {Position{1, 13}, tokenInteger, "0"}, + {Position{1, 14}, tokenComma, ","}, + {Position{1, 16}, tokenKey, "'123'.'45abc'"}, + {Position{1, 30}, tokenEqual, "="}, + {Position{1, 32}, tokenInteger, "0"}, + {Position{1, 34}, tokenRightCurlyBrace, "}"}, + {Position{1, 35}, tokenEOF, ""}, + }) +} + +func TestLexInlineTableDottedKey3(t *testing.T) { + testFlow(t, `foo = { a = 0, "123"."45ʎǝʞ" = 0 }`, []token{ + {Position{1, 1}, tokenKey, "foo"}, + {Position{1, 5}, tokenEqual, "="}, + {Position{1, 7}, tokenLeftCurlyBrace, "{"}, + {Position{1, 9}, tokenKey, "a"}, + {Position{1, 11}, tokenEqual, "="}, + {Position{1, 13}, tokenInteger, "0"}, + {Position{1, 14}, tokenComma, ","}, + {Position{1, 16}, tokenKey, `"123"."45ʎǝʞ"`}, + {Position{1, 30}, tokenEqual, "="}, + {Position{1, 32}, tokenInteger, "0"}, + {Position{1, 34}, tokenRightCurlyBrace, "}"}, + {Position{1, 35}, tokenEOF, ""}, + }) +} + +func TestLexInlineTableBareKeyWithComma(t *testing.T) { + testFlow(t, `foo = { -bar1 = "baz", -bar_ = "baz" }`, []token{ + {Position{1, 1}, tokenKey, "foo"}, + {Position{1, 5}, tokenEqual, "="}, + {Position{1, 7}, tokenLeftCurlyBrace, "{"}, + {Position{1, 9}, tokenKey, "-bar1"}, + {Position{1, 15}, tokenEqual, "="}, + {Position{1, 18}, tokenString, "baz"}, + {Position{1, 22}, tokenComma, ","}, + {Position{1, 24}, tokenKey, "-bar_"}, + {Position{1, 30}, tokenEqual, "="}, + {Position{1, 33}, tokenString, "baz"}, + {Position{1, 38}, tokenRightCurlyBrace, "}"}, + {Position{1, 39}, tokenEOF, ""}, + }) +} + func TestLexInlineTableBareKeyUnderscore(t *testing.T) { testFlow(t, `foo = { _bar = "baz" }`, []token{ {Position{1, 1}, tokenKey, "foo"}, diff -Nru golang-github-pelletier-go-toml-1.6.0/Makefile golang-github-pelletier-go-toml-1.8.1/Makefile --- golang-github-pelletier-go-toml-1.6.0/Makefile 1970-01-01 00:00:00.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/Makefile 2020-09-12 18:42:04.000000000 +0000 @@ -0,0 +1,29 @@ +export CGO_ENABLED=0 +go := go +go.goos ?= $(shell echo `go version`|cut -f4 -d ' '|cut -d '/' -f1) +go.goarch ?= $(shell echo `go version`|cut -f4 -d ' '|cut -d '/' -f2) + +out.tools := tomll tomljson jsontoml +out.dist := $(out.tools:=_$(go.goos)_$(go.goarch).tar.xz) +sources := $(wildcard **/*.go) + + +.PHONY: +tools: $(out.tools) + +$(out.tools): $(sources) + GOOS=$(go.goos) GOARCH=$(go.goarch) $(go) build ./cmd/$@ + +.PHONY: +dist: $(out.dist) + +$(out.dist):%_$(go.goos)_$(go.goarch).tar.xz: % + if [ "$(go.goos)" = "windows" ]; then \ + tar -cJf $@ $^.exe; \ + else \ + tar -cJf $@ $^; \ + fi + +.PHONY: +clean: + rm -rf $(out.tools) $(out.dist) diff -Nru golang-github-pelletier-go-toml-1.6.0/marshal.go golang-github-pelletier-go-toml-1.8.1/marshal.go --- golang-github-pelletier-go-toml-1.6.0/marshal.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/marshal.go 2020-09-12 18:42:04.000000000 +0000 @@ -2,6 +2,7 @@ import ( "bytes" + "encoding" "errors" "fmt" "io" @@ -22,6 +23,7 @@ type tomlOpts struct { name string + nameFromTag bool comment string commented bool multiline bool @@ -68,9 +70,13 @@ var timeType = reflect.TypeOf(time.Time{}) var marshalerType = reflect.TypeOf(new(Marshaler)).Elem() +var unmarshalerType = reflect.TypeOf(new(Unmarshaler)).Elem() +var textMarshalerType = reflect.TypeOf(new(encoding.TextMarshaler)).Elem() +var textUnmarshalerType = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem() var localDateType = reflect.TypeOf(LocalDate{}) var localTimeType = reflect.TypeOf(LocalTime{}) var localDateTimeType = reflect.TypeOf(LocalDateTime{}) +var mapStringInterfaceType = reflect.TypeOf(map[string]interface{}{}) // Check if the given marshal type maps to a Tree primitive func isPrimitive(mtype reflect.Type) bool { @@ -88,12 +94,16 @@ case reflect.String: return true case reflect.Struct: - return mtype == timeType || mtype == localDateType || mtype == localDateTimeType || mtype == localTimeType || isCustomMarshaler(mtype) + return isTimeType(mtype) default: return false } } +func isTimeType(mtype reflect.Type) bool { + return mtype == timeType || mtype == localDateType || mtype == localDateTimeType || mtype == localTimeType +} + // Check if the given marshal type maps to a Tree slice or array func isTreeSequence(mtype reflect.Type) bool { switch mtype.Kind() { @@ -106,6 +116,30 @@ } } +// Check if the given marshal type maps to a slice or array of a custom marshaler type +func isCustomMarshalerSequence(mtype reflect.Type) bool { + switch mtype.Kind() { + case reflect.Ptr: + return isCustomMarshalerSequence(mtype.Elem()) + case reflect.Slice, reflect.Array: + return isCustomMarshaler(mtype.Elem()) || isCustomMarshaler(reflect.New(mtype.Elem()).Type()) + default: + return false + } +} + +// Check if the given marshal type maps to a slice or array of a text marshaler type +func isTextMarshalerSequence(mtype reflect.Type) bool { + switch mtype.Kind() { + case reflect.Ptr: + return isTextMarshalerSequence(mtype.Elem()) + case reflect.Slice, reflect.Array: + return isTextMarshaler(mtype.Elem()) || isTextMarshaler(reflect.New(mtype.Elem()).Type()) + default: + return false + } +} + // Check if the given marshal type maps to a non-Tree slice or array func isOtherSequence(mtype reflect.Type) bool { switch mtype.Kind() { @@ -140,12 +174,42 @@ return mval.Interface().(Marshaler).MarshalTOML() } +func isTextMarshaler(mtype reflect.Type) bool { + return mtype.Implements(textMarshalerType) && !isTimeType(mtype) +} + +func callTextMarshaler(mval reflect.Value) ([]byte, error) { + return mval.Interface().(encoding.TextMarshaler).MarshalText() +} + +func isCustomUnmarshaler(mtype reflect.Type) bool { + return mtype.Implements(unmarshalerType) +} + +func callCustomUnmarshaler(mval reflect.Value, tval interface{}) error { + return mval.Interface().(Unmarshaler).UnmarshalTOML(tval) +} + +func isTextUnmarshaler(mtype reflect.Type) bool { + return mtype.Implements(textUnmarshalerType) +} + +func callTextUnmarshaler(mval reflect.Value, text []byte) error { + return mval.Interface().(encoding.TextUnmarshaler).UnmarshalText(text) +} + // Marshaler is the interface implemented by types that // can marshal themselves into valid TOML. type Marshaler interface { MarshalTOML() ([]byte, error) } +// Unmarshaler is the interface implemented by types that +// can unmarshal a TOML description of themselves. +type Unmarshaler interface { + UnmarshalTOML(interface{}) error +} + /* Marshal returns the TOML encoding of v. Behavior is similar to the Go json encoder, except that there is no concept of a Marshaler interface or MarshalTOML @@ -190,20 +254,23 @@ w io.Writer encOpts annotation - line int - col int - order marshalOrder + line int + col int + order marshalOrder + promoteAnon bool + indentation string } // NewEncoder returns a new encoder that writes to w. func NewEncoder(w io.Writer) *Encoder { return &Encoder{ - w: w, - encOpts: encOptsDefaults, - annotation: annotationDefault, - line: 0, - col: 1, - order: OrderAlphabetical, + w: w, + encOpts: encOptsDefaults, + annotation: annotationDefault, + line: 0, + col: 1, + order: OrderAlphabetical, + indentation: " ", } } @@ -255,6 +322,12 @@ return e } +// Indentation allows to change indentation when marshalling. +func (e *Encoder) Indentation(indent string) *Encoder { + e.indentation = indent + return e +} + // SetTagName allows changing default tag "toml" func (e *Encoder) SetTagName(v string) *Encoder { e.tag = v @@ -279,8 +352,31 @@ return e } +// PromoteAnonymous allows to change how anonymous struct fields are marshaled. +// Usually, they are marshaled as if the inner exported fields were fields in +// the outer struct. However, if an anonymous struct field is given a name in +// its TOML tag, it is treated like a regular struct field with that name. +// rather than being anonymous. +// +// In case anonymous promotion is enabled, all anonymous structs are promoted +// and treated like regular struct fields. +func (e *Encoder) PromoteAnonymous(promote bool) *Encoder { + e.promoteAnon = promote + return e +} + func (e *Encoder) marshal(v interface{}) ([]byte, error) { + // Check if indentation is valid + for _, char := range e.indentation { + if !isSpace(char) { + return []byte{}, fmt.Errorf("invalid indentation: must only contains space or tab characters") + } + } + mtype := reflect.TypeOf(v) + if mtype == nil { + return []byte{}, errors.New("nil cannot be marshaled to TOML") + } switch mtype.Kind() { case reflect.Struct, reflect.Map: @@ -288,6 +384,9 @@ if mtype.Elem().Kind() != reflect.Struct { return []byte{}, errors.New("Only pointer to struct can be marshaled to TOML") } + if reflect.ValueOf(v).IsNil() { + return []byte{}, errors.New("nil pointer cannot be marshaled to TOML") + } default: return []byte{}, errors.New("Only a struct or map can be marshaled to TOML") } @@ -296,13 +395,16 @@ if isCustomMarshaler(mtype) { return callCustomMarshaler(sval) } + if isTextMarshaler(mtype) { + return callTextMarshaler(sval) + } t, err := e.valueToTree(mtype, sval) if err != nil { return []byte{}, err } var buf bytes.Buffer - _, err = t.writeToOrdered(&buf, "", "", 0, e.arraysOneElementPerLine, e.order) + _, err = t.writeToOrdered(&buf, "", "", 0, e.arraysOneElementPerLine, e.order, e.indentation, false) return buf.Bytes(), err } @@ -320,20 +422,29 @@ tval := e.nextTree() switch mtype.Kind() { case reflect.Struct: - for i := 0; i < mtype.NumField(); i++ { - mtypef, mvalf := mtype.Field(i), mval.Field(i) - opts := tomlOptions(mtypef, e.annotation) - if opts.include && (!opts.omitempty || !isZero(mvalf)) { - val, err := e.valueToToml(mtypef.Type, mvalf) - if err != nil { - return nil, err + switch mval.Interface().(type) { + case Tree: + reflect.ValueOf(tval).Elem().Set(mval) + default: + for i := 0; i < mtype.NumField(); i++ { + mtypef, mvalf := mtype.Field(i), mval.Field(i) + opts := tomlOptions(mtypef, e.annotation) + if opts.include && ((mtypef.Type.Kind() != reflect.Interface && !opts.omitempty) || !isZero(mvalf)) { + val, err := e.valueToToml(mtypef.Type, mvalf) + if err != nil { + return nil, err + } + if tree, ok := val.(*Tree); ok && mtypef.Anonymous && !opts.nameFromTag && !e.promoteAnon { + e.appendTree(tval, tree) + } else { + val = e.wrapTomlValue(val, tval) + tval.SetPathWithOptions([]string{opts.name}, SetOptions{ + Comment: opts.comment, + Commented: opts.commented, + Multiline: opts.multiline, + }, val) + } } - - tval.SetWithOptions(opts.name, SetOptions{ - Comment: opts.comment, - Commented: opts.commented, - Multiline: opts.multiline, - }, val) } } case reflect.Map: @@ -358,18 +469,22 @@ } for _, key := range keys { mvalf := mval.MapIndex(key) + if (mtype.Elem().Kind() == reflect.Ptr || mtype.Elem().Kind() == reflect.Interface) && mvalf.IsNil() { + continue + } val, err := e.valueToToml(mtype.Elem(), mvalf) if err != nil { return nil, err } + val = e.wrapTomlValue(val, tval) if e.quoteMapKeys { - keyStr, err := tomlValueStringRepresentation(key.String(), "", e.arraysOneElementPerLine) + keyStr, err := tomlValueStringRepresentation(key.String(), "", "", e.order, e.arraysOneElementPerLine) if err != nil { return nil, err } tval.SetPath([]string{keyStr}, val) } else { - tval.Set(key.String(), val) + tval.SetPath([]string{key.String()}, val) } } } @@ -404,19 +519,32 @@ // Convert given marshal value to toml value func (e *Encoder) valueToToml(mtype reflect.Type, mval reflect.Value) (interface{}, error) { - e.line++ if mtype.Kind() == reflect.Ptr { - return e.valueToToml(mtype.Elem(), mval.Elem()) + switch { + case isCustomMarshaler(mtype): + return callCustomMarshaler(mval) + case isTextMarshaler(mtype): + b, err := callTextMarshaler(mval) + return string(b), err + default: + return e.valueToToml(mtype.Elem(), mval.Elem()) + } + } + if mtype.Kind() == reflect.Interface { + return e.valueToToml(mval.Elem().Type(), mval.Elem()) } switch { case isCustomMarshaler(mtype): return callCustomMarshaler(mval) + case isTextMarshaler(mtype): + b, err := callTextMarshaler(mval) + return string(b), err case isTree(mtype): return e.valueToTree(mtype, mval) + case isOtherSequence(mtype), isCustomMarshalerSequence(mtype), isTextMarshalerSequence(mtype): + return e.valueToOtherSlice(mtype, mval) case isTreeSequence(mtype): return e.valueToTreeSlice(mtype, mval) - case isOtherSequence(mtype): - return e.valueToOtherSlice(mtype, mval) default: switch mtype.Kind() { case reflect.Bool: @@ -440,6 +568,38 @@ } } +func (e *Encoder) appendTree(t, o *Tree) error { + for key, value := range o.values { + if _, ok := t.values[key]; ok { + continue + } + if tomlValue, ok := value.(*tomlValue); ok { + tomlValue.position.Col = t.position.Col + } + t.values[key] = value + } + return nil +} + +// Create a toml value with the current line number as the position line +func (e *Encoder) wrapTomlValue(val interface{}, parent *Tree) interface{} { + _, isTree := val.(*Tree) + _, isTreeS := val.([]*Tree) + if isTree || isTreeS { + return val + } + + ret := &tomlValue{ + value: val, + position: Position{ + e.line, + parent.position.Col, + }, + } + e.line++ + return ret +} + // Unmarshal attempts to unmarshal the Tree into a Go struct pointed by v. // Neither Unmarshaler interfaces nor UnmarshalTOML functions are supported for // sub-structs, and only definite types can be unmarshaled. @@ -492,6 +652,8 @@ tval *Tree encOpts tagName string + strict bool + visitor visitorState } // NewDecoder returns a new decoder that reads from r. @@ -522,8 +684,18 @@ return d } +// Strict allows changing to strict decoding. Any fields that are found in the +// input data and do not have a corresponding struct member cause an error. +func (d *Decoder) Strict(strict bool) *Decoder { + d.strict = strict + return d +} + func (d *Decoder) unmarshal(v interface{}) error { mtype := reflect.TypeOf(v) + if mtype == nil { + return errors.New("nil cannot be unmarshaled from TOML") + } if mtype.Kind() != reflect.Ptr { return errors.New("only a pointer to struct or map can be unmarshaled from TOML") } @@ -532,16 +704,29 @@ switch elem.Kind() { case reflect.Struct, reflect.Map: + case reflect.Interface: + elem = mapStringInterfaceType default: return errors.New("only a pointer to struct or map can be unmarshaled from TOML") } + if reflect.ValueOf(v).IsNil() { + return errors.New("nil pointer cannot be unmarshaled from TOML") + } + vv := reflect.ValueOf(v).Elem() + if d.strict { + d.visitor = newVisitorState(d.tval) + } + sval, err := d.valueFromTree(elem, d.tval, &vv) if err != nil { return err } + if err := d.visitor.validate(); err != nil { + return err + } reflect.ValueOf(v).Elem().Set(sval) return nil } @@ -552,6 +737,21 @@ if mtype.Kind() == reflect.Ptr { return d.unwrapPointer(mtype, tval, mval1) } + + // Check if pointer to value implements the Unmarshaler interface. + if mvalPtr := reflect.New(mtype); isCustomUnmarshaler(mvalPtr.Type()) { + d.visitor.visitAll() + + if tval == nil { + return mvalPtr.Elem(), nil + } + + if err := callCustomUnmarshaler(mvalPtr, tval.ToMap()); err != nil { + return reflect.ValueOf(nil), fmt.Errorf("unmarshal toml: %v", err) + } + return mvalPtr.Elem(), nil + } + var mval reflect.Value switch mtype.Kind() { case reflect.Struct: @@ -561,11 +761,17 @@ mval = reflect.New(mtype).Elem() } - for i := 0; i < mtype.NumField(); i++ { - mtypef := mtype.Field(i) - an := annotation{tag: d.tagName} - opts := tomlOptions(mtypef, an) - if opts.include { + switch mval.Interface().(type) { + case Tree: + mval.Set(reflect.ValueOf(tval).Elem()) + default: + for i := 0; i < mtype.NumField(); i++ { + mtypef := mtype.Field(i) + an := annotation{tag: d.tagName} + opts := tomlOptions(mtypef, an) + if !opts.include { + continue + } baseKey := opts.name keysToTry := []string{ baseKey, @@ -575,20 +781,25 @@ } found := false - for _, key := range keysToTry { - exists := tval.Has(key) - if !exists { - continue - } - val := tval.Get(key) - fval := mval.Field(i) - mvalf, err := d.valueFromToml(mtypef.Type, val, &fval) - if err != nil { - return mval, formatError(err, tval.GetPosition(key)) + if tval != nil { + for _, key := range keysToTry { + exists := tval.HasPath([]string{key}) + if !exists { + continue + } + + d.visitor.push(key) + val := tval.GetPath([]string{key}) + fval := mval.Field(i) + mvalf, err := d.valueFromToml(mtypef.Type, val, &fval) + if err != nil { + return mval, formatError(err, tval.GetPositionPath([]string{key})) + } + mval.Field(i).Set(mvalf) + found = true + d.visitor.pop() + break } - mval.Field(i).Set(mvalf) - found = true - break } if !found && opts.defaultValue != "" { @@ -596,37 +807,52 @@ var val interface{} var err error switch mvalf.Kind() { + case reflect.String: + val = opts.defaultValue case reflect.Bool: val, err = strconv.ParseBool(opts.defaultValue) - if err != nil { - return mval.Field(i), err - } + case reflect.Uint: + val, err = strconv.ParseUint(opts.defaultValue, 10, 0) + case reflect.Uint8: + val, err = strconv.ParseUint(opts.defaultValue, 10, 8) + case reflect.Uint16: + val, err = strconv.ParseUint(opts.defaultValue, 10, 16) + case reflect.Uint32: + val, err = strconv.ParseUint(opts.defaultValue, 10, 32) + case reflect.Uint64: + val, err = strconv.ParseUint(opts.defaultValue, 10, 64) case reflect.Int: - val, err = strconv.Atoi(opts.defaultValue) - if err != nil { - return mval.Field(i), err - } - case reflect.String: - val = opts.defaultValue + val, err = strconv.ParseInt(opts.defaultValue, 10, 0) + case reflect.Int8: + val, err = strconv.ParseInt(opts.defaultValue, 10, 8) + case reflect.Int16: + val, err = strconv.ParseInt(opts.defaultValue, 10, 16) + case reflect.Int32: + val, err = strconv.ParseInt(opts.defaultValue, 10, 32) case reflect.Int64: val, err = strconv.ParseInt(opts.defaultValue, 10, 64) - if err != nil { - return mval.Field(i), err - } + case reflect.Float32: + val, err = strconv.ParseFloat(opts.defaultValue, 32) case reflect.Float64: val, err = strconv.ParseFloat(opts.defaultValue, 64) - if err != nil { - return mval.Field(i), err - } default: - return mval.Field(i), fmt.Errorf("unsuported field type for default option") + return mvalf, fmt.Errorf("unsupported field type for default option") + } + + if err != nil { + return mvalf, err } - mval.Field(i).Set(reflect.ValueOf(val)) + mvalf.Set(reflect.ValueOf(val).Convert(mvalf.Type())) } - // save the old behavior above and try to check anonymous structs - if !found && opts.defaultValue == "" && mtypef.Anonymous && mtypef.Type.Kind() == reflect.Struct { - v, err := d.valueFromTree(mtypef.Type, tval, nil) + // save the old behavior above and try to check structs + if !found && opts.defaultValue == "" && mtypef.Type.Kind() == reflect.Struct { + tmpTval := tval + if !mtypef.Anonymous { + tmpTval = nil + } + fval := mval.Field(i) + v, err := d.valueFromTree(mtypef.Type, tmpTval, &fval) if err != nil { return v, err } @@ -637,13 +863,15 @@ case reflect.Map: mval = reflect.MakeMap(mtype) for _, key := range tval.Keys() { + d.visitor.push(key) // TODO: path splits key val := tval.GetPath([]string{key}) mvalf, err := d.valueFromToml(mtype.Elem(), val, nil) if err != nil { - return mval, formatError(err, tval.GetPosition(key)) + return mval, formatError(err, tval.GetPositionPath([]string{key})) } mval.SetMapIndex(reflect.ValueOf(key).Convert(mtype.Key()), mvalf) + d.visitor.pop() } } return mval, nil @@ -651,20 +879,30 @@ // Convert toml value to marshal struct/map slice, using marshal type func (d *Decoder) valueFromTreeSlice(mtype reflect.Type, tval []*Tree) (reflect.Value, error) { - mval := reflect.MakeSlice(mtype, len(tval), len(tval)) + mval, err := makeSliceOrArray(mtype, len(tval)) + if err != nil { + return mval, err + } + for i := 0; i < len(tval); i++ { + d.visitor.push(strconv.Itoa(i)) val, err := d.valueFromTree(mtype.Elem(), tval[i], nil) if err != nil { return mval, err } mval.Index(i).Set(val) + d.visitor.pop() } return mval, nil } // Convert toml value to marshal primitive slice, using marshal type func (d *Decoder) valueFromOtherSlice(mtype reflect.Type, tval []interface{}) (reflect.Value, error) { - mval := reflect.MakeSlice(mtype, len(tval), len(tval)) + mval, err := makeSliceOrArray(mtype, len(tval)) + if err != nil { + return mval, err + } + for i := 0; i < len(tval); i++ { val, err := d.valueFromToml(mtype.Elem(), tval[i], nil) if err != nil { @@ -675,6 +913,41 @@ return mval, nil } +// Convert toml value to marshal primitive slice, using marshal type +func (d *Decoder) valueFromOtherSliceI(mtype reflect.Type, tval interface{}) (reflect.Value, error) { + val := reflect.ValueOf(tval) + length := val.Len() + + mval, err := makeSliceOrArray(mtype, length) + if err != nil { + return mval, err + } + + for i := 0; i < length; i++ { + val, err := d.valueFromToml(mtype.Elem(), val.Index(i).Interface(), nil) + if err != nil { + return mval, err + } + mval.Index(i).Set(val) + } + return mval, nil +} + +// Create a new slice or a new array with specified length +func makeSliceOrArray(mtype reflect.Type, tLength int) (reflect.Value, error) { + var mval reflect.Value + switch mtype.Kind() { + case reflect.Slice: + mval = reflect.MakeSlice(mtype, tLength, tLength) + case reflect.Array: + mval = reflect.New(reflect.ArrayOf(mtype.Len(), mtype.Elem())).Elem() + if tLength > mtype.Len() { + return mval, fmt.Errorf("unmarshal: TOML array length (%v) exceeds destination array length (%v)", tLength, mtype.Len()) + } + } + return mval, nil +} + // Convert toml value to marshal value, using marshal type. When mval1 is non-nil // and the given type is a struct value, merge fields into it. func (d *Decoder) valueFromToml(mtype reflect.Type, tval interface{}, mval1 *reflect.Value) (reflect.Value, error) { @@ -692,18 +965,53 @@ if isTree(mtype) { return d.valueFromTree(mtype, t, mval11) } + + if mtype.Kind() == reflect.Interface { + if mval1 == nil || mval1.IsNil() { + return d.valueFromTree(reflect.TypeOf(map[string]interface{}{}), t, nil) + } else { + return d.valueFromToml(mval1.Elem().Type(), t, nil) + } + } + return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to a tree", tval, tval) case []*Tree: if isTreeSequence(mtype) { return d.valueFromTreeSlice(mtype, t) } + if mtype.Kind() == reflect.Interface { + if mval1 == nil || mval1.IsNil() { + return d.valueFromTreeSlice(reflect.TypeOf([]map[string]interface{}{}), t) + } else { + ival := mval1.Elem() + return d.valueFromToml(mval1.Elem().Type(), t, &ival) + } + } return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to trees", tval, tval) case []interface{}: + d.visitor.visit() if isOtherSequence(mtype) { return d.valueFromOtherSlice(mtype, t) } + if mtype.Kind() == reflect.Interface { + if mval1 == nil || mval1.IsNil() { + return d.valueFromOtherSlice(reflect.TypeOf([]interface{}{}), t) + } else { + ival := mval1.Elem() + return d.valueFromToml(mval1.Elem().Type(), t, &ival) + } + } return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to a slice", tval, tval) default: + d.visitor.visit() + // Check if pointer to value implements the encoding.TextUnmarshaler. + if mvalPtr := reflect.New(mtype); isTextUnmarshaler(mvalPtr.Type()) && !isTimeType(mtype) { + if err := d.unmarshalText(tval, mvalPtr); err != nil { + return reflect.ValueOf(nil), fmt.Errorf("unmarshal text: %v", err) + } + return mvalPtr.Elem(), nil + } + switch mtype.Kind() { case reflect.Bool, reflect.Struct: val := reflect.ValueOf(tval) @@ -754,38 +1062,50 @@ } return reflect.ValueOf(d), nil } - if !val.Type().ConvertibleTo(mtype) { + if !val.Type().ConvertibleTo(mtype) || val.Kind() == reflect.Float64 { return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String()) } - if reflect.Indirect(reflect.New(mtype)).OverflowInt(val.Convert(mtype).Int()) { + if reflect.Indirect(reflect.New(mtype)).OverflowInt(val.Convert(reflect.TypeOf(int64(0))).Int()) { return reflect.ValueOf(nil), fmt.Errorf("%v(%T) would overflow %v", tval, tval, mtype.String()) } return val.Convert(mtype), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: val := reflect.ValueOf(tval) - if !val.Type().ConvertibleTo(mtype) { + if !val.Type().ConvertibleTo(mtype) || val.Kind() == reflect.Float64 { return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String()) } if val.Convert(reflect.TypeOf(int(1))).Int() < 0 { return reflect.ValueOf(nil), fmt.Errorf("%v(%T) is negative so does not fit in %v", tval, tval, mtype.String()) } - if reflect.Indirect(reflect.New(mtype)).OverflowUint(uint64(val.Convert(mtype).Uint())) { + if reflect.Indirect(reflect.New(mtype)).OverflowUint(val.Convert(reflect.TypeOf(uint64(0))).Uint()) { return reflect.ValueOf(nil), fmt.Errorf("%v(%T) would overflow %v", tval, tval, mtype.String()) } return val.Convert(mtype), nil case reflect.Float32, reflect.Float64: val := reflect.ValueOf(tval) - if !val.Type().ConvertibleTo(mtype) { + if !val.Type().ConvertibleTo(mtype) || val.Kind() == reflect.Int64 { return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v", tval, tval, mtype.String()) } - if reflect.Indirect(reflect.New(mtype)).OverflowFloat(val.Convert(mtype).Float()) { + if reflect.Indirect(reflect.New(mtype)).OverflowFloat(val.Convert(reflect.TypeOf(float64(0))).Float()) { return reflect.ValueOf(nil), fmt.Errorf("%v(%T) would overflow %v", tval, tval, mtype.String()) } return val.Convert(mtype), nil + case reflect.Interface: + if mval1 == nil || mval1.IsNil() { + return reflect.ValueOf(tval), nil + } else { + ival := mval1.Elem() + return d.valueFromToml(mval1.Elem().Type(), t, &ival) + } + case reflect.Slice, reflect.Array: + if isOtherSequence(mtype) && isOtherSequence(reflect.TypeOf(t)) { + return d.valueFromOtherSliceI(mtype, t) + } + return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v(%v)", tval, tval, mtype, mtype.Kind()) default: return reflect.ValueOf(nil), fmt.Errorf("Can't convert %v(%T) to %v(%v)", tval, tval, mtype, mtype.Kind()) } @@ -795,7 +1115,7 @@ func (d *Decoder) unwrapPointer(mtype reflect.Type, tval interface{}, mval1 *reflect.Value) (reflect.Value, error) { var melem *reflect.Value - if mval1 != nil && !mval1.IsNil() && mtype.Elem().Kind() == reflect.Struct { + if mval1 != nil && !mval1.IsNil() && (mtype.Elem().Kind() == reflect.Struct || mtype.Elem().Kind() == reflect.Interface) { elem := mval1.Elem() melem = &elem } @@ -809,6 +1129,12 @@ return mval, nil } +func (d *Decoder) unmarshalText(tval interface{}, mval reflect.Value) error { + var buf bytes.Buffer + fmt.Fprint(&buf, tval) + return callTextUnmarshaler(mval, buf.Bytes()) +} + func tomlOptions(vf reflect.StructField, an annotation) tomlOpts { tag := vf.Tag.Get(an.tag) parse := strings.Split(tag, ",") @@ -821,6 +1147,7 @@ defaultValue := vf.Tag.Get(tagDefault) result := tomlOpts{ name: vf.Name, + nameFromTag: false, comment: comment, commented: commented, multiline: multiline, @@ -833,6 +1160,7 @@ result.include = false } else { result.name = strings.Trim(parse[0], " ") + result.nameFromTag = true } } if vf.PkgPath != "" { @@ -849,11 +1177,7 @@ func isZero(val reflect.Value) bool { switch val.Type().Kind() { - case reflect.Map: - fallthrough - case reflect.Array: - fallthrough - case reflect.Slice: + case reflect.Slice, reflect.Array, reflect.Map: return val.Len() == 0 default: return reflect.DeepEqual(val.Interface(), reflect.Zero(val.Type()).Interface()) @@ -866,3 +1190,80 @@ } return fmt.Errorf("%s: %s", pos, err) } + +// visitorState keeps track of which keys were unmarshaled. +type visitorState struct { + tree *Tree + path []string + keys map[string]struct{} + active bool +} + +func newVisitorState(tree *Tree) visitorState { + path, result := []string{}, map[string]struct{}{} + insertKeys(path, result, tree) + return visitorState{ + tree: tree, + path: path[:0], + keys: result, + active: true, + } +} + +func (s *visitorState) push(key string) { + if s.active { + s.path = append(s.path, key) + } +} + +func (s *visitorState) pop() { + if s.active { + s.path = s.path[:len(s.path)-1] + } +} + +func (s *visitorState) visit() { + if s.active { + delete(s.keys, strings.Join(s.path, ".")) + } +} + +func (s *visitorState) visitAll() { + if s.active { + for k := range s.keys { + if strings.HasPrefix(k, strings.Join(s.path, ".")) { + delete(s.keys, k) + } + } + } +} + +func (s *visitorState) validate() error { + if !s.active { + return nil + } + undecoded := make([]string, 0, len(s.keys)) + for key := range s.keys { + undecoded = append(undecoded, key) + } + sort.Strings(undecoded) + if len(undecoded) > 0 { + return fmt.Errorf("undecoded keys: %q", undecoded) + } + return nil +} + +func insertKeys(path []string, m map[string]struct{}, tree *Tree) { + for k, v := range tree.values { + switch node := v.(type) { + case []*Tree: + for i, item := range node { + insertKeys(append(path, k, strconv.Itoa(i)), m, item) + } + case *Tree: + insertKeys(append(path, k), m, node) + case *tomlValue: + m[strings.Join(append(path, k), ".")] = struct{}{} + } + } +} diff -Nru golang-github-pelletier-go-toml-1.6.0/marshal_test.go golang-github-pelletier-go-toml-1.8.1/marshal_test.go --- golang-github-pelletier-go-toml-1.6.0/marshal_test.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/marshal_test.go 2020-09-12 18:42:04.000000000 +0000 @@ -7,53 +7,76 @@ "io/ioutil" "os" "reflect" + "strconv" "strings" "testing" "time" ) type basicMarshalTestStruct struct { - String string `toml:"Zstring"` - StringList []string `toml:"Ystrlist"` - Sub basicMarshalTestSubStruct `toml:"Xsubdoc"` - SubList []basicMarshalTestSubStruct `toml:"Wsublist"` + String string `toml:"Zstring"` + StringList []string `toml:"Ystrlist"` + BasicMarshalTestSubAnonymousStruct + Sub basicMarshalTestSubStruct `toml:"Xsubdoc"` + SubList []basicMarshalTestSubStruct `toml:"Wsublist"` } type basicMarshalTestSubStruct struct { String2 string } +type BasicMarshalTestSubAnonymousStruct struct { + String3 string +} + var basicTestData = basicMarshalTestStruct{ - String: "Hello", - StringList: []string{"Howdy", "Hey There"}, - Sub: basicMarshalTestSubStruct{"One"}, - SubList: []basicMarshalTestSubStruct{{"Two"}, {"Three"}}, + String: "Hello", + StringList: []string{"Howdy", "Hey There"}, + BasicMarshalTestSubAnonymousStruct: BasicMarshalTestSubAnonymousStruct{"One"}, + Sub: basicMarshalTestSubStruct{"Two"}, + SubList: []basicMarshalTestSubStruct{{"Three"}, {"Four"}}, } -var basicTestToml = []byte(`Ystrlist = ["Howdy","Hey There"] +var basicTestToml = []byte(`String3 = "One" +Ystrlist = ["Howdy", "Hey There"] Zstring = "Hello" [[Wsublist]] + String2 = "Three" + +[[Wsublist]] + String2 = "Four" + +[Xsubdoc] String2 = "Two" +`) + +var basicTestTomlCustomIndentation = []byte(`String3 = "One" +Ystrlist = ["Howdy", "Hey There"] +Zstring = "Hello" [[Wsublist]] - String2 = "Three" + String2 = "Three" + +[[Wsublist]] + String2 = "Four" [Xsubdoc] - String2 = "One" + String2 = "Two" `) var basicTestTomlOrdered = []byte(`Zstring = "Hello" -Ystrlist = ["Howdy","Hey There"] +Ystrlist = ["Howdy", "Hey There"] +String3 = "One" [Xsubdoc] - String2 = "One" - -[[Wsublist]] String2 = "Two" [[Wsublist]] String2 = "Three" + +[[Wsublist]] + String2 = "Four" `) var marshalTestToml = []byte(`title = "TOML Marshal Testing" @@ -68,12 +91,12 @@ uint = 5001 [basic_lists] - bools = [true,false,true] - dates = [1979-05-27T07:32:00Z,1980-05-27T07:32:00Z] - floats = [12.3,45.6,78.9] - ints = [8001,8001,8002] - strings = ["One","Two","Three"] - uints = [5002,5003] + bools = [true, false, true] + dates = [1979-05-27T07:32:00Z, 1980-05-27T07:32:00Z] + floats = [12.3, 45.6, 78.9] + ints = [8001, 8001, 8002] + strings = ["One", "Two", "Three"] + uints = [5002, 5003] [basic_map] one = "one" @@ -100,12 +123,12 @@ var marshalOrderPreserveToml = []byte(`title = "TOML Marshal Testing" [basic_lists] - floats = [12.3,45.6,78.9] - bools = [true,false,true] - dates = [1979-05-27T07:32:00Z,1980-05-27T07:32:00Z] - ints = [8001,8001,8002] - uints = [5002,5003] - strings = ["One","Two","Three"] + floats = [12.3, 45.6, 78.9] + bools = [true, false, true] + dates = [1979-05-27T07:32:00Z, 1980-05-27T07:32:00Z] + ints = [8001, 8001, 8002] + uints = [5002, 5003] + strings = ["One", "Two", "Three"] [[subdocptrs]] name = "Second" @@ -157,6 +180,44 @@ j9 = "10" `) +type Conf struct { + Name string + Age int + Inter interface{} +} + +type NestedStruct struct { + FirstName string + LastName string + Age int +} + +var doc = []byte(`Name = "rui" +Age = 18 + +[Inter] + FirstName = "wang" + LastName = "jl" + Age = 100`) + +func TestInterface(t *testing.T) { + var config Conf + config.Inter = &NestedStruct{} + err := Unmarshal(doc, &config) + expected := Conf{ + Name: "rui", + Age: 18, + Inter: &NestedStruct{ + FirstName: "wang", + LastName: "jl", + Age: 100, + }, + } + if err != nil || !reflect.DeepEqual(config, expected) { + t.Errorf("Bad unmarshal: expected %v, got %v", expected, config) + } +} + func TestBasicMarshal(t *testing.T) { result, err := Marshal(basicTestData) if err != nil { @@ -168,6 +229,26 @@ } } +func TestBasicMarshalCustomIndentation(t *testing.T) { + var result bytes.Buffer + err := NewEncoder(&result).Indentation("\t").Encode(basicTestData) + if err != nil { + t.Fatal(err) + } + expected := basicTestTomlCustomIndentation + if !bytes.Equal(result.Bytes(), expected) { + t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result.Bytes()) + } +} + +func TestBasicMarshalWrongIndentation(t *testing.T) { + var result bytes.Buffer + err := NewEncoder(&result).Indentation(" \n").Encode(basicTestData) + if err.Error() != "invalid indentation: must only contains space or tab characters" { + t.Error("expect err:invalid indentation: must only contains space or tab characters but got:", err) + } +} + func TestBasicMarshalOrdered(t *testing.T) { var result bytes.Buffer err := NewEncoder(&result).Order(OrderPreserve).Encode(basicTestData) @@ -215,6 +296,59 @@ } } +type quotedKeyMarshalTestStruct struct { + String string `toml:"Z.string-àéù"` + Float float64 `toml:"Yfloat-𝟘"` + Sub basicMarshalTestSubStruct `toml:"Xsubdoc-àéù"` + SubList []basicMarshalTestSubStruct `toml:"W.sublist-𝟘"` +} + +var quotedKeyMarshalTestData = quotedKeyMarshalTestStruct{ + String: "Hello", + Float: 3.5, + Sub: basicMarshalTestSubStruct{"One"}, + SubList: []basicMarshalTestSubStruct{{"Two"}, {"Three"}}, +} + +var quotedKeyMarshalTestToml = []byte(`"Yfloat-𝟘" = 3.5 +"Z.string-àéù" = "Hello" + +[["W.sublist-𝟘"]] + String2 = "Two" + +[["W.sublist-𝟘"]] + String2 = "Three" + +["Xsubdoc-àéù"] + String2 = "One" +`) + +func TestBasicMarshalQuotedKey(t *testing.T) { + result, err := Marshal(quotedKeyMarshalTestData) + if err != nil { + t.Fatal(err) + } + expected := quotedKeyMarshalTestToml + if !bytes.Equal(result, expected) { + t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result) + } +} + +func TestBasicUnmarshalQuotedKey(t *testing.T) { + tree, err := LoadBytes(quotedKeyMarshalTestToml) + if err != nil { + t.Fatal(err) + } + + var q quotedKeyMarshalTestStruct + tree.Unmarshal(&q) + fmt.Println(q) + + if !reflect.DeepEqual(quotedKeyMarshalTestData, q) { + t.Errorf("Bad unmarshal: expected\n-----\n%v\n-----\ngot\n-----\n%v\n-----\n", quotedKeyMarshalTestData, q) + } +} + type testDoc struct { Title string `toml:"title"` BasicLists testDocBasicLists `toml:"basic_lists"` @@ -788,8 +922,8 @@ StringPtr: &strPtr2, } -var nestedTestToml = []byte(`String = [["Five","Six"],["One","Two"]] -StringPtr = [["Three","Four"]] +var nestedTestToml = []byte(`String = [["Five", "Six"], ["One", "Two"]] +StringPtr = [["Three", "Four"]] `) func TestNestedMarshal(t *testing.T) { @@ -821,24 +955,27 @@ } type customMarshaler struct { - FirsName string - LastName string + FirstName string + LastName string } func (c customMarshaler) MarshalTOML() ([]byte, error) { - fullName := fmt.Sprintf("%s %s", c.FirsName, c.LastName) + fullName := fmt.Sprintf("%s %s", c.FirstName, c.LastName) return []byte(fullName), nil } -var customMarshalerData = customMarshaler{FirsName: "Sally", LastName: "Fields"} +var customMarshalerData = customMarshaler{FirstName: "Sally", LastName: "Fields"} var customMarshalerToml = []byte(`Sally Fields`) var nestedCustomMarshalerData = customMarshalerParent{ - Self: customMarshaler{FirsName: "Maiku", LastName: "Suteda"}, + Self: customMarshaler{FirstName: "Maiku", LastName: "Suteda"}, Friends: []customMarshaler{customMarshalerData}, } var nestedCustomMarshalerToml = []byte(`friends = ["Sally Fields"] me = "Maiku Suteda" `) +var nestedCustomMarshalerTomlForUnmarshal = []byte(`[friends] +FirstName = "Sally" +LastName = "Fields"`) func TestCustomMarshaler(t *testing.T) { result, err := Marshal(customMarshalerData) @@ -851,14 +988,206 @@ } } +type IntOrString string + +func (x *IntOrString) MarshalTOML() ([]byte, error) { + s := *(*string)(x) + _, err := strconv.Atoi(s) + if err != nil { + return []byte(fmt.Sprintf(`"%s"`, s)), nil + } + return []byte(s), nil +} + func TestNestedCustomMarshaler(t *testing.T) { - result, err := Marshal(nestedCustomMarshalerData) + num := IntOrString("100") + str := IntOrString("hello") + var parent = struct { + IntField *IntOrString `toml:"int"` + StringField *IntOrString `toml:"string"` + }{ + &num, + &str, + } + + result, err := Marshal(parent) if err != nil { t.Fatal(err) } - expected := nestedCustomMarshalerToml - if !bytes.Equal(result, expected) { - t.Errorf("Bad nested custom marshaler: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result) + expected := `int = 100 +string = "hello" +` + if !bytes.Equal(result, []byte(expected)) { + t.Errorf("Bad nested text marshaler: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result) + } +} + +type textMarshaler struct { + FirstName string + LastName string +} + +func (m textMarshaler) MarshalText() ([]byte, error) { + fullName := fmt.Sprintf("%s %s", m.FirstName, m.LastName) + return []byte(fullName), nil +} + +func TestTextMarshaler(t *testing.T) { + m := textMarshaler{FirstName: "Sally", LastName: "Fields"} + + result, err := Marshal(m) + if err != nil { + t.Fatal(err) + } + expected := `Sally Fields` + if !bytes.Equal(result, []byte(expected)) { + t.Errorf("Bad text marshaler: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result) + } +} + +func TestUnmarshalTextMarshaler(t *testing.T) { + var nested = struct { + Friends textMarshaler `toml:"friends"` + }{} + + var expected = struct { + Friends textMarshaler `toml:"friends"` + }{ + Friends: textMarshaler{FirstName: "Sally", LastName: "Fields"}, + } + + err := Unmarshal(nestedCustomMarshalerTomlForUnmarshal, &nested) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(nested, expected) { + t.Errorf("Bad unmarshal: expected %v, got %v", expected, nested) + } +} + +func TestNestedTextMarshaler(t *testing.T) { + var parent = struct { + Self textMarshaler `toml:"me"` + Friends []textMarshaler `toml:"friends"` + Stranger *textMarshaler `toml:"stranger"` + }{ + Self: textMarshaler{FirstName: "Maiku", LastName: "Suteda"}, + Friends: []textMarshaler{textMarshaler{FirstName: "Sally", LastName: "Fields"}}, + Stranger: &textMarshaler{FirstName: "Earl", LastName: "Henson"}, + } + + result, err := Marshal(parent) + if err != nil { + t.Fatal(err) + } + expected := `friends = ["Sally Fields"] +me = "Maiku Suteda" +stranger = "Earl Henson" +` + if !bytes.Equal(result, []byte(expected)) { + t.Errorf("Bad nested text marshaler: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result) + } +} + +type precedentMarshaler struct { + FirstName string + LastName string +} + +func (m precedentMarshaler) MarshalText() ([]byte, error) { + return []byte("shadowed"), nil +} + +func (m precedentMarshaler) MarshalTOML() ([]byte, error) { + fullName := fmt.Sprintf("%s %s", m.FirstName, m.LastName) + return []byte(fullName), nil +} + +func TestPrecedentMarshaler(t *testing.T) { + m := textMarshaler{FirstName: "Sally", LastName: "Fields"} + + result, err := Marshal(m) + if err != nil { + t.Fatal(err) + } + expected := `Sally Fields` + if !bytes.Equal(result, []byte(expected)) { + t.Errorf("Bad text marshaler: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result) + } +} + +type customPointerMarshaler struct { + FirstName string + LastName string +} + +func (m *customPointerMarshaler) MarshalTOML() ([]byte, error) { + return []byte(`"hidden"`), nil +} + +type textPointerMarshaler struct { + FirstName string + LastName string +} + +func (m *textPointerMarshaler) MarshalText() ([]byte, error) { + return []byte("hidden"), nil +} + +func TestPointerMarshaler(t *testing.T) { + var parent = struct { + Self customPointerMarshaler `toml:"me"` + Stranger *customPointerMarshaler `toml:"stranger"` + Friend textPointerMarshaler `toml:"friend"` + Fiend *textPointerMarshaler `toml:"fiend"` + }{ + Self: customPointerMarshaler{FirstName: "Maiku", LastName: "Suteda"}, + Stranger: &customPointerMarshaler{FirstName: "Earl", LastName: "Henson"}, + Friend: textPointerMarshaler{FirstName: "Sally", LastName: "Fields"}, + Fiend: &textPointerMarshaler{FirstName: "Casper", LastName: "Snider"}, + } + + result, err := Marshal(parent) + if err != nil { + t.Fatal(err) + } + expected := `fiend = "hidden" +stranger = "hidden" + +[friend] + FirstName = "Sally" + LastName = "Fields" + +[me] + FirstName = "Maiku" + LastName = "Suteda" +` + if !bytes.Equal(result, []byte(expected)) { + t.Errorf("Bad nested text marshaler: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, result) + } +} + +func TestPointerCustomMarshalerSequence(t *testing.T) { + var customPointerMarshalerSlice *[]*customPointerMarshaler + var customPointerMarshalerArray *[2]*customPointerMarshaler + + if !isCustomMarshalerSequence(reflect.TypeOf(customPointerMarshalerSlice)) { + t.Errorf("error: should be a sequence of custom marshaler interfaces") + } + if !isCustomMarshalerSequence(reflect.TypeOf(customPointerMarshalerArray)) { + t.Errorf("error: should be a sequence of custom marshaler interfaces") + } +} + +func TestPointerTextMarshalerSequence(t *testing.T) { + var textPointerMarshalerSlice *[]*textPointerMarshaler + var textPointerMarshalerArray *[2]*textPointerMarshaler + + if !isTextMarshalerSequence(reflect.TypeOf(textPointerMarshalerSlice)) { + t.Errorf("error: should be a sequence of text marshaler interfaces") + } + if !isTextMarshalerSequence(reflect.TypeOf(textPointerMarshalerArray)) { + t.Errorf("error: should be a sequence of text marshaler interfaces") } } @@ -913,6 +1242,213 @@ } } +func TestMarshalMultilineCommented(t *testing.T) { + expectedToml := []byte(`# MultilineArray = [ + # 100, + # 200, + # 300, +# ] +# MultilineNestedArray = [ + # [ + # "a", + # "b", + # "c", +# ], + # [ + # "d", + # "e", + # "f", +# ], +# ] +# MultilineString = """ +# I +# am +# Allen""" +NonCommented = "Not commented line" +`) + type StructWithMultiline struct { + NonCommented string + MultilineString string `commented:"true" multiline:"true"` + MultilineArray []int `commented:"true"` + MultilineNestedArray [][]string `commented:"true"` + } + + var buf bytes.Buffer + enc := NewEncoder(&buf) + if err := enc.ArraysWithOneElementPerLine(true).Encode(StructWithMultiline{ + NonCommented: "Not commented line", + MultilineString: "I\nam\nAllen", + MultilineArray: []int{100, 200, 300}, + MultilineNestedArray: [][]string{ + {"a", "b", "c"}, + {"d", "e", "f"}, + }, + }); err == nil { + result := buf.Bytes() + if !bytes.Equal(result, expectedToml) { + t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expectedToml, result) + } + } else { + t.Fatal(err) + } +} + +func TestMarshalNonPrimitiveTypeCommented(t *testing.T) { + expectedToml := []byte(` +# [CommentedMapField] + + # [CommentedMapField.CommentedMapField1] + # SingleLineString = "This line should be commented out" + + # [CommentedMapField.CommentedMapField2] + # SingleLineString = "This line should be commented out" + +# [CommentedStructField] + + # [CommentedStructField.CommentedStructField] + # MultilineArray = [ + # 1, + # 2, + # ] + # MultilineNestedArray = [ + # [ + # 10, + # 20, + # ], + # [ + # 100, + # 200, + # ], + # ] + # MultilineString = """ +# This line +# should be +# commented out""" + + # [CommentedStructField.NotCommentedStructField] + # MultilineArray = [ + # 1, + # 2, + # ] + # MultilineNestedArray = [ + # [ + # 10, + # 20, + # ], + # [ + # 100, + # 200, + # ], + # ] + # MultilineString = """ +# This line +# should be +# commented out""" + +[NotCommentedStructField] + + # [NotCommentedStructField.CommentedStructField] + # MultilineArray = [ + # 1, + # 2, + # ] + # MultilineNestedArray = [ + # [ + # 10, + # 20, + # ], + # [ + # 100, + # 200, + # ], + # ] + # MultilineString = """ +# This line +# should be +# commented out""" + + [NotCommentedStructField.NotCommentedStructField] + MultilineArray = [ + 3, + 4, + ] + MultilineNestedArray = [ + [ + 30, + 40, + ], + [ + 300, + 400, + ], + ] + MultilineString = """ +This line +should NOT be +commented out""" +`) + type InnerStruct struct { + MultilineString string `multiline:"true"` + MultilineArray []int + MultilineNestedArray [][]int + } + type MiddleStruct struct { + NotCommentedStructField InnerStruct + CommentedStructField InnerStruct `commented:"true"` + } + type OuterStruct struct { + CommentedStructField MiddleStruct `commented:"true"` + NotCommentedStructField MiddleStruct + CommentedMapField map[string]struct{ SingleLineString string } `commented:"true"` + } + + commentedTestStruct := OuterStruct{ + CommentedStructField: MiddleStruct{ + NotCommentedStructField: InnerStruct{ + MultilineString: "This line\nshould be\ncommented out", + MultilineArray: []int{1, 2}, + MultilineNestedArray: [][]int{{10, 20}, {100, 200}}, + }, + CommentedStructField: InnerStruct{ + MultilineString: "This line\nshould be\ncommented out", + MultilineArray: []int{1, 2}, + MultilineNestedArray: [][]int{{10, 20}, {100, 200}}, + }, + }, + NotCommentedStructField: MiddleStruct{ + NotCommentedStructField: InnerStruct{ + MultilineString: "This line\nshould NOT be\ncommented out", + MultilineArray: []int{3, 4}, + MultilineNestedArray: [][]int{{30, 40}, {300, 400}}, + }, + CommentedStructField: InnerStruct{ + MultilineString: "This line\nshould be\ncommented out", + MultilineArray: []int{1, 2}, + MultilineNestedArray: [][]int{{10, 20}, {100, 200}}, + }, + }, + CommentedMapField: map[string]struct{ SingleLineString string }{ + "CommentedMapField1": { + SingleLineString: "This line should be commented out", + }, + "CommentedMapField2": { + SingleLineString: "This line should be commented out", + }, + }, + } + + var buf bytes.Buffer + enc := NewEncoder(&buf) + if err := enc.ArraysWithOneElementPerLine(true).Encode(commentedTestStruct); err == nil { + result := buf.Bytes() + if !bytes.Equal(result, expectedToml) { + t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expectedToml, result) + } + } else { + t.Fatal(err) + } +} + type mapsTestStruct struct { Simple map[string]string Paths map[string]string @@ -998,7 +1534,7 @@ func TestMarshalArray(t *testing.T) { expected := []byte(` [A] - B = [1,2,3] + B = [1, 2, 3] C = [1] `) @@ -1174,6 +1710,57 @@ } } +func TestUnmarshalTabInStringAndQuotedKey(t *testing.T) { + type Test struct { + Field1 string `toml:"Fie ld1"` + Field2 string + } + + type TestCase struct { + desc string + input []byte + expected Test + } + + testCases := []TestCase{ + { + desc: "multiline string with tab", + input: []byte("Field2 = \"\"\"\nhello\tworld\"\"\""), + expected: Test{ + Field2: "hello\tworld", + }, + }, + { + desc: "quoted key with tab", + input: []byte("\"Fie\tld1\" = \"key with tab\""), + expected: Test{ + Field1: "key with tab", + }, + }, + { + desc: "basic string tab", + input: []byte("Field2 = \"hello\tworld\""), + expected: Test{ + Field2: "hello\tworld", + }, + }, + } + + for i := range testCases { + result := Test{} + err := Unmarshal(testCases[i].input, &result) + if err != nil { + t.Errorf("%s test error:%v", testCases[i].desc, err) + continue + } + + if !reflect.DeepEqual(result, testCases[i].expected) { + t.Errorf("%s test error: expected\n-----\n%+v\n-----\ngot\n-----\n%+v\n-----\n", + testCases[i].desc, testCases[i].expected, result) + } + } +} + var customMultilineTagTestToml = []byte(`int_slice = [ 1, 2, @@ -1199,6 +1786,103 @@ } } +func TestMultilineWithAdjacentQuotationMarks(t *testing.T) { + type testStruct struct { + Str string `multiline:"true"` + } + type testCase struct { + expected []byte + data testStruct + } + + testCases := []testCase{ + { + expected: []byte(`Str = """ +hello\"""" +`), + data: testStruct{ + Str: "hello\"", + }, + }, + { + expected: []byte(`Str = """ +""\"""\"""\"""" +`), + data: testStruct{ + Str: "\"\"\"\"\"\"\"\"\"", + }, + }, + } + for i := range testCases { + result, err := Marshal(testCases[i].data) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(result, testCases[i].expected) { + t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", + testCases[i].expected, result) + } else { + var data testStruct + if err = Unmarshal(result, &data); err != nil { + t.Fatal(err) + } + if data.Str != testCases[i].data.Str { + t.Errorf("Round trip test fail: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", + testCases[i].data.Str, data.Str) + } + } + } +} + +func TestMarshalEmbedTree(t *testing.T) { + expected := []byte(`OuterField1 = "Out" +OuterField2 = 1024 + +[TreeField] + InnerField1 = "In" + InnerField2 = 2048 + + [TreeField.EmbedStruct] + EmbedField = "Embed" +`) + type InnerStruct struct { + InnerField1 string + InnerField2 int + EmbedStruct struct { + EmbedField string + } + } + + type OuterStruct struct { + OuterField1 string + OuterField2 int + TreeField *Tree + } + + tree, err := Load(` +InnerField1 = "In" +InnerField2 = 2048 + +[EmbedStruct] + EmbedField = "Embed" +`) + if err != nil { + t.Fatal(err) + } + + out := OuterStruct{ + "Out", + 1024, + tree, + } + actual, _ := Marshal(out) + + if !bytes.Equal(actual, expected) { + t.Errorf("Bad marshal: expected %s, got %s", expected, actual) + } +} + var testDocBasicToml = []byte(` [document] bool_val = true @@ -1347,6 +2031,58 @@ } } +func TestMarshalNestedArrayInlineTables(t *testing.T) { + type table struct { + Value1 int `toml:"ZValue1"` + Value2 int `toml:"YValue2"` + Value3 int `toml:"XValue3"` + } + + type nestedTable struct { + Table table + } + + nestedArray := struct { + Simple [][]table + SimplePointer *[]*[]table + Nested [][]nestedTable + NestedPointer *[]*[]nestedTable + }{ + Simple: [][]table{{{Value1: 1}, {Value1: 10}}}, + SimplePointer: &[]*[]table{{{Value2: 2}}}, + Nested: [][]nestedTable{{{Table: table{Value3: 3}}}}, + NestedPointer: &[]*[]nestedTable{{{Table: table{Value3: -3}}}}, + } + + expectedPreserve := `Simple = [[{ ZValue1 = 1, YValue2 = 0, XValue3 = 0 }, { ZValue1 = 10, YValue2 = 0, XValue3 = 0 }]] +SimplePointer = [[{ ZValue1 = 0, YValue2 = 2, XValue3 = 0 }]] +Nested = [[{ Table = { ZValue1 = 0, YValue2 = 0, XValue3 = 3 } }]] +NestedPointer = [[{ Table = { ZValue1 = 0, YValue2 = 0, XValue3 = -3 } }]] +` + + expectedAlphabetical := `Nested = [[{ Table = { XValue3 = 3, YValue2 = 0, ZValue1 = 0 } }]] +NestedPointer = [[{ Table = { XValue3 = -3, YValue2 = 0, ZValue1 = 0 } }]] +Simple = [[{ XValue3 = 0, YValue2 = 0, ZValue1 = 1 }, { XValue3 = 0, YValue2 = 0, ZValue1 = 10 }]] +SimplePointer = [[{ XValue3 = 0, YValue2 = 2, ZValue1 = 0 }]] +` + + var bufPreserve bytes.Buffer + if err := NewEncoder(&bufPreserve).Order(OrderPreserve).Encode(nestedArray); err != nil { + t.Fatalf("unexpected error: %s", err.Error()) + } + if !bytes.Equal(bufPreserve.Bytes(), []byte(expectedPreserve)) { + t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expectedPreserve, bufPreserve.String()) + } + + var bufAlphabetical bytes.Buffer + if err := NewEncoder(&bufAlphabetical).Order(OrderAlphabetical).Encode(nestedArray); err != nil { + t.Fatalf("unexpected error: %s", err.Error()) + } + if !bytes.Equal(bufAlphabetical.Bytes(), []byte(expectedAlphabetical)) { + t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expectedAlphabetical, bufAlphabetical.String()) + } +} + type testDuration struct { Nanosec time.Duration `toml:"nanosec"` Microsec1 time.Duration `toml:"microsec1"` @@ -1458,10 +2194,7 @@ result := testBadDuration{} err := NewDecoder(buf).Decode(&result) if err == nil { - t.Fatal() - } - if err.Error() != "(1, 1): Can't convert 1z(string) to time.Duration. time: unknown unit z in duration 1z" { - t.Fatalf("unexpected error: %s", err) + t.Fatal("expected bad duration error") } } @@ -1482,16 +2215,98 @@ } } -func TestUnmarshalDefault(t *testing.T) { - var doc struct { - StringField string `default:"a"` - BoolField bool `default:"true"` - IntField int `default:"1"` - Int64Field int64 `default:"2"` - Float64Field float64 `default:"3.1"` +func TestUnmarshalNegativeUint(t *testing.T) { + type check struct{ U uint } + + tree, _ := Load("u = -1") + err := tree.Unmarshal(&check{}) + if err.Error() != "(1, 1): -1(int64) is negative so does not fit in uint" { + t.Error("expect err:(1, 1): -1(int64) is negative so does not fit in uint but got:", err) } +} - err := Unmarshal([]byte(``), &doc) +func TestUnmarshalCheckConversionFloatInt(t *testing.T) { + type conversionCheck struct { + U uint + I int + F float64 + } + + treeU, _ := Load("u = 1e300") + treeI, _ := Load("i = 1e300") + treeF, _ := Load("f = 9223372036854775806") + + errU := treeU.Unmarshal(&conversionCheck{}) + errI := treeI.Unmarshal(&conversionCheck{}) + errF := treeF.Unmarshal(&conversionCheck{}) + + if errU.Error() != "(1, 1): Can't convert 1e+300(float64) to uint" { + t.Error("expect err:(1, 1): Can't convert 1e+300(float64) to uint but got:", errU) + } + if errI.Error() != "(1, 1): Can't convert 1e+300(float64) to int" { + t.Error("expect err:(1, 1): Can't convert 1e+300(float64) to int but got:", errI) + } + if errF.Error() != "(1, 1): Can't convert 9223372036854775806(int64) to float64" { + t.Error("expect err:(1, 1): Can't convert 9223372036854775806(int64) to float64 but got:", errF) + } +} + +func TestUnmarshalOverflow(t *testing.T) { + type overflow struct { + U8 uint8 + I8 int8 + F32 float32 + } + + treeU8, _ := Load("u8 = 300") + treeI8, _ := Load("i8 = 300") + treeF32, _ := Load("f32 = 1e300") + + errU8 := treeU8.Unmarshal(&overflow{}) + errI8 := treeI8.Unmarshal(&overflow{}) + errF32 := treeF32.Unmarshal(&overflow{}) + + if errU8.Error() != "(1, 1): 300(int64) would overflow uint8" { + t.Error("expect err:(1, 1): 300(int64) would overflow uint8 but got:", errU8) + } + if errI8.Error() != "(1, 1): 300(int64) would overflow int8" { + t.Error("expect err:(1, 1): 300(int64) would overflow int8 but got:", errI8) + } + if errF32.Error() != "(1, 1): 1e+300(float64) would overflow float32" { + t.Error("expect err:(1, 1): 1e+300(float64) would overflow float32 but got:", errF32) + } +} + +func TestUnmarshalDefault(t *testing.T) { + type EmbeddedStruct struct { + StringField string `default:"c"` + } + + type aliasUint uint + + var doc struct { + StringField string `default:"a"` + BoolField bool `default:"true"` + UintField uint `default:"1"` + Uint8Field uint8 `default:"8"` + Uint16Field uint16 `default:"16"` + Uint32Field uint32 `default:"32"` + Uint64Field uint64 `default:"64"` + IntField int `default:"-1"` + Int8Field int8 `default:"-8"` + Int16Field int16 `default:"-16"` + Int32Field int32 `default:"-32"` + Int64Field int64 `default:"-64"` + Float32Field float32 `default:"32.1"` + Float64Field float64 `default:"64.1"` + NonEmbeddedStruct struct { + StringField string `default:"b"` + } + EmbeddedStruct + AliasUintField aliasUint `default:"1000"` + } + + err := Unmarshal([]byte(``), &doc) if err != nil { t.Fatal(err) } @@ -1501,14 +2316,50 @@ if doc.StringField != "a" { t.Errorf("StringField should be \"a\", not %s", doc.StringField) } - if doc.IntField != 1 { - t.Errorf("IntField should be 1, not %d", doc.IntField) + if doc.UintField != 1 { + t.Errorf("UintField should be 1, not %d", doc.UintField) + } + if doc.Uint8Field != 8 { + t.Errorf("Uint8Field should be 8, not %d", doc.Uint8Field) + } + if doc.Uint16Field != 16 { + t.Errorf("Uint16Field should be 16, not %d", doc.Uint16Field) + } + if doc.Uint32Field != 32 { + t.Errorf("Uint32Field should be 32, not %d", doc.Uint32Field) + } + if doc.Uint64Field != 64 { + t.Errorf("Uint64Field should be 64, not %d", doc.Uint64Field) + } + if doc.IntField != -1 { + t.Errorf("IntField should be -1, not %d", doc.IntField) + } + if doc.Int8Field != -8 { + t.Errorf("Int8Field should be -8, not %d", doc.Int8Field) + } + if doc.Int16Field != -16 { + t.Errorf("Int16Field should be -16, not %d", doc.Int16Field) + } + if doc.Int32Field != -32 { + t.Errorf("Int32Field should be -32, not %d", doc.Int32Field) + } + if doc.Int64Field != -64 { + t.Errorf("Int64Field should be -64, not %d", doc.Int64Field) } - if doc.Int64Field != 2 { - t.Errorf("Int64Field should be 2, not %d", doc.Int64Field) + if doc.Float32Field != 32.1 { + t.Errorf("Float32Field should be 32.1, not %f", doc.Float32Field) } - if doc.Float64Field != 3.1 { - t.Errorf("Float64Field should be 3.1, not %f", doc.Float64Field) + if doc.Float64Field != 64.1 { + t.Errorf("Float64Field should be 64.1, not %f", doc.Float64Field) + } + if doc.NonEmbeddedStruct.StringField != "b" { + t.Errorf("StringField should be \"b\", not %s", doc.NonEmbeddedStruct.StringField) + } + if doc.EmbeddedStruct.StringField != "c" { + t.Errorf("StringField should be \"c\", not %s", doc.EmbeddedStruct.StringField) + } + if doc.AliasUintField != 1000 { + t.Errorf("AliasUintField should be 1000, not %d", doc.AliasUintField) } } @@ -1567,6 +2418,99 @@ } } +func TestMarshalNestedAnonymousStructs(t *testing.T) { + type Embedded struct { + Value string `toml:"value"` + Top struct { + Value string `toml:"value"` + } `toml:"top"` + } + + type Named struct { + Value string `toml:"value"` + } + + var doc struct { + Embedded + Named `toml:"named"` + Anonymous struct { + Value string `toml:"value"` + } `toml:"anonymous"` + } + + expected := `value = "" + +[anonymous] + value = "" + +[named] + value = "" + +[top] + value = "" +` + + result, err := Marshal(doc) + if err != nil { + t.Fatalf("unexpected error: %s", err.Error()) + } + if !bytes.Equal(result, []byte(expected)) { + t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, string(result)) + } +} + +func TestEncoderPromoteNestedAnonymousStructs(t *testing.T) { + type Embedded struct { + Value string `toml:"value"` + } + + var doc struct { + Embedded + } + + expected := ` +[Embedded] + value = "" +` + var buf bytes.Buffer + if err := NewEncoder(&buf).PromoteAnonymous(true).Encode(doc); err != nil { + t.Fatalf("unexpected error: %s", err.Error()) + } + if !bytes.Equal(buf.Bytes(), []byte(expected)) { + t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, buf.String()) + } +} + +func TestMarshalNestedAnonymousStructs_DuplicateField(t *testing.T) { + type Embedded struct { + Value string `toml:"value"` + Top struct { + Value string `toml:"value"` + } `toml:"top"` + } + + var doc struct { + Value string `toml:"value"` + Embedded + } + doc.Embedded.Value = "shadowed" + doc.Value = "shadows" + + expected := `value = "shadows" + +[top] + value = "" +` + + result, err := Marshal(doc) + if err != nil { + t.Fatalf("unexpected error: %s", err.Error()) + } + if !bytes.Equal(result, []byte(expected)) { + t.Errorf("Bad marshal: expected\n-----\n%s\n-----\ngot\n-----\n%s\n-----\n", expected, string(result)) + } +} + func TestUnmarshalNestedAnonymousStructs(t *testing.T) { type Nested struct { Value string `toml:"nested_field"` @@ -1644,7 +2588,7 @@ [[slice1]] exported1 = "visible3" - + [[slice1]] exported1 = "visible4" @@ -1750,7 +2694,7 @@ }{ XY: [2]int{1, 2}, }, - Expected: `XY = [1,2] + Expected: `XY = [1, 2] `, }, { @@ -1759,7 +2703,7 @@ }{ XY: [1][2]int{{1, 2}}, }, - Expected: `XY = [[1,2]] + Expected: `XY = [[1, 2]] `, }, { @@ -1768,7 +2712,7 @@ }{ XY: [1][]int{{1, 2}}, }, - Expected: `XY = [[1,2]] + Expected: `XY = [[1, 2]] `, }, { @@ -1777,7 +2721,7 @@ }{ XY: [][2]int{{1, 2}}, }, - Expected: `XY = [[1,2]] + Expected: `XY = [[1, 2]] `, }, } @@ -2140,3 +3084,895 @@ }) } } + +// test case for issue #339 +func TestUnmarshalSameInnerField(t *testing.T) { + type InterStruct2 struct { + Test string + Name string + Age int + } + type Inter2 struct { + Name string + Age int + InterStruct2 InterStruct2 + } + type Server struct { + Name string `toml:"name"` + Inter2 Inter2 `toml:"inter2"` + } + + var server Server + + if err := Unmarshal([]byte(`name = "123" +[inter2] +name = "inter2" +age = 222`), &server); err == nil { + expected := Server{ + Name: "123", + Inter2: Inter2{ + Name: "inter2", + Age: 222, + }, + } + if !reflect.DeepEqual(server, expected) { + t.Errorf("Bad unmarshal: expected %v, got %v", expected, server) + } + } else { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestMarshalInterface(t *testing.T) { + type InnerStruct struct { + InnerField string + } + + type OuterStruct struct { + PrimitiveField interface{} + ArrayField interface{} + StructArrayField interface{} + MapField map[string]interface{} + StructField interface{} + PointerField interface{} + NilField interface{} + InterfacePointerField *interface{} + } + + expected := []byte(`ArrayField = [1, 2, 3] +InterfacePointerField = "hello world" +PrimitiveField = "string" + +[MapField] + key1 = "value1" + key2 = false + + [MapField.key3] + InnerField = "value3" + +[PointerField] + InnerField = "yyy" + +[[StructArrayField]] + InnerField = "s1" + +[[StructArrayField]] + InnerField = "s2" + +[StructField] + InnerField = "xxx" +`) + + var h interface{} = "hello world" + if result, err := Marshal(OuterStruct{ + "string", + []int{1, 2, 3}, + []InnerStruct{{"s1"}, {"s2"}}, + map[string]interface{}{ + "key1": "value1", + "key2": false, + "key3": InnerStruct{"value3"}, + "nil value": nil, + }, + InnerStruct{ + "xxx", + }, + &InnerStruct{ + "yyy", + }, + nil, + &h, + }); err == nil { + if !bytes.Equal(result, expected) { + t.Errorf("Bad marshal: expected\n----\n%s\n----\ngot\n----\n%s\n----\n", expected, result) + } + } else { + t.Fatal(err) + } +} + +func TestUnmarshalToNilInterface(t *testing.T) { + toml := []byte(` +PrimitiveField = "Hello" +ArrayField = [1,2,3] +InterfacePointerField = "World" + +[StructField] +Field1 = 123 +Field2 = "Field2" + +[MapField] +MapField1 = [4,5,6] +MapField2 = {A = "A"} +MapField3 = false + +[[StructArrayField]] +Name = "Allen" +Age = 20 + +[[StructArrayField]] +Name = "Jack" +Age = 23 +`) + + type OuterStruct struct { + PrimitiveField interface{} + ArrayField interface{} + StructArrayField interface{} + MapField map[string]interface{} + StructField interface{} + NilField interface{} + InterfacePointerField *interface{} + } + + var s interface{} = "World" + expected := OuterStruct{ + PrimitiveField: "Hello", + ArrayField: []interface{}{int64(1), int64(2), int64(3)}, + StructField: map[string]interface{}{ + "Field1": int64(123), + "Field2": "Field2", + }, + MapField: map[string]interface{}{ + "MapField1": []interface{}{int64(4), int64(5), int64(6)}, + "MapField2": map[string]interface{}{ + "A": "A", + }, + "MapField3": false, + }, + NilField: nil, + InterfacePointerField: &s, + StructArrayField: []map[string]interface{}{ + { + "Name": "Allen", + "Age": int64(20), + }, + { + "Name": "Jack", + "Age": int64(23), + }, + }, + } + actual := OuterStruct{} + if err := Unmarshal(toml, &actual); err == nil { + if !reflect.DeepEqual(actual, expected) { + t.Errorf("Bad unmarshal: expected %v, got %v", expected, actual) + } + } else { + t.Fatal(err) + } +} + +func TestUnmarshalToNonNilInterface(t *testing.T) { + toml := []byte(` +PrimitiveField = "Allen" +ArrayField = [1,2,3] + +[StructField] +InnerField = "After1" + +[PointerField] +InnerField = "After2" + +[InterfacePointerField] +InnerField = "After" + +[MapField] +MapField1 = [4,5,6] +MapField2 = {A = "A"} +MapField3 = false + +[[StructArrayField]] +InnerField = "After3" + +[[StructArrayField]] +InnerField = "After4" +`) + type InnerStruct struct { + InnerField interface{} + } + + type OuterStruct struct { + PrimitiveField interface{} + ArrayField interface{} + StructArrayField interface{} + MapField map[string]interface{} + StructField interface{} + PointerField interface{} + NilField interface{} + InterfacePointerField *interface{} + } + + var s interface{} = InnerStruct{"After"} + expected := OuterStruct{ + PrimitiveField: "Allen", + ArrayField: []int{1, 2, 3}, + StructField: InnerStruct{InnerField: "After1"}, + MapField: map[string]interface{}{ + "MapField1": []interface{}{int64(4), int64(5), int64(6)}, + "MapField2": map[string]interface{}{ + "A": "A", + }, + "MapField3": false, + }, + PointerField: &InnerStruct{InnerField: "After2"}, + NilField: nil, + InterfacePointerField: &s, + StructArrayField: []InnerStruct{ + {InnerField: "After3"}, + {InnerField: "After4"}, + }, + } + actual := OuterStruct{ + PrimitiveField: "aaa", + ArrayField: []int{100, 200, 300, 400}, + StructField: InnerStruct{InnerField: "Before1"}, + MapField: map[string]interface{}{ + "MapField1": []int{4, 5, 6}, + "MapField2": map[string]string{ + "B": "BBB", + }, + "MapField3": true, + }, + PointerField: &InnerStruct{InnerField: "Before2"}, + NilField: nil, + InterfacePointerField: &s, + StructArrayField: []InnerStruct{ + {InnerField: "Before3"}, + {InnerField: "Before4"}, + }, + } + if err := Unmarshal(toml, &actual); err == nil { + if !reflect.DeepEqual(actual, expected) { + t.Errorf("Bad unmarshal: expected %v, got %v", expected, actual) + } + } else { + t.Fatal(err) + } +} + +func TestUnmarshalEmbedTree(t *testing.T) { + toml := []byte(` +OuterField1 = "Out" +OuterField2 = 1024 + +[TreeField] +InnerField1 = "In" +InnerField2 = 2048 + + [TreeField.EmbedStruct] + EmbedField = "Embed" + +`) + type InnerStruct struct { + InnerField1 string + InnerField2 int + EmbedStruct struct { + EmbedField string + } + } + + type OuterStruct struct { + OuterField1 string + OuterField2 int + TreeField *Tree + } + + out := OuterStruct{} + actual := InnerStruct{} + expected := InnerStruct{ + "In", + 2048, + struct { + EmbedField string + }{ + EmbedField: "Embed", + }, + } + if err := Unmarshal(toml, &out); err != nil { + t.Fatal(err) + } + if err := out.TreeField.Unmarshal(&actual); err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(actual, expected) { + t.Errorf("Bad unmarshal: expected %v, got %v", expected, actual) + } +} + +func TestMarshalNil(t *testing.T) { + if _, err := Marshal(nil); err == nil { + t.Errorf("Expected err from nil marshal") + } + if _, err := Marshal((*struct{})(nil)); err == nil { + t.Errorf("Expected err from nil marshal") + } +} + +func TestUnmarshalNil(t *testing.T) { + if err := Unmarshal([]byte(`whatever = "whatever"`), nil); err == nil { + t.Errorf("Expected err from nil marshal") + } + if err := Unmarshal([]byte(`whatever = "whatever"`), (*struct{})(nil)); err == nil { + t.Errorf("Expected err from nil marshal") + } +} + +var sliceTomlDemo = []byte(`str_slice = ["Howdy","Hey There"] +str_slice_ptr= ["Howdy","Hey There"] +int_slice=[1,2] +int_slice_ptr=[1,2] +[[struct_slice]] +String2="1" +[[struct_slice]] +String2="2" +[[struct_slice_ptr]] +String2="1" +[[struct_slice_ptr]] +String2="2" +`) + +type sliceStruct struct { + Slice []string ` toml:"str_slice" ` + SlicePtr *[]string ` toml:"str_slice_ptr" ` + IntSlice []int ` toml:"int_slice" ` + IntSlicePtr *[]int ` toml:"int_slice_ptr" ` + StructSlice []basicMarshalTestSubStruct ` toml:"struct_slice" ` + StructSlicePtr *[]basicMarshalTestSubStruct ` toml:"struct_slice_ptr" ` +} + +type arrayStruct struct { + Slice [4]string ` toml:"str_slice" ` + SlicePtr *[4]string ` toml:"str_slice_ptr" ` + IntSlice [4]int ` toml:"int_slice" ` + IntSlicePtr *[4]int ` toml:"int_slice_ptr" ` + StructSlice [4]basicMarshalTestSubStruct ` toml:"struct_slice" ` + StructSlicePtr *[4]basicMarshalTestSubStruct ` toml:"struct_slice_ptr" ` +} + +type arrayTooSmallStruct struct { + Slice [1]string ` toml:"str_slice" ` + StructSlice [1]basicMarshalTestSubStruct ` toml:"struct_slice" ` +} + +func TestUnmarshalSlice(t *testing.T) { + tree, _ := LoadBytes(sliceTomlDemo) + tree, _ = TreeFromMap(tree.ToMap()) + + var actual sliceStruct + err := tree.Unmarshal(&actual) + if err != nil { + t.Error("shound not err", err) + } + expected := sliceStruct{ + Slice: []string{"Howdy", "Hey There"}, + SlicePtr: &[]string{"Howdy", "Hey There"}, + IntSlice: []int{1, 2}, + IntSlicePtr: &[]int{1, 2}, + StructSlice: []basicMarshalTestSubStruct{{"1"}, {"2"}}, + StructSlicePtr: &[]basicMarshalTestSubStruct{{"1"}, {"2"}}, + } + if !reflect.DeepEqual(actual, expected) { + t.Errorf("Bad unmarshal: expected %v, got %v", expected, actual) + } + +} + +func TestUnmarshalSliceFail(t *testing.T) { + tree, _ := TreeFromMap(map[string]interface{}{ + "str_slice": []int{1, 2}, + }) + + var actual sliceStruct + err := tree.Unmarshal(&actual) + if err.Error() != "(0, 0): Can't convert 1(int64) to string" { + t.Error("expect err:(0, 0): Can't convert 1(int64) to string but got ", err) + } +} + +func TestUnmarshalSliceFail2(t *testing.T) { + tree, _ := Load(`str_slice=[1,2]`) + + var actual sliceStruct + err := tree.Unmarshal(&actual) + if err.Error() != "(1, 1): Can't convert 1(int64) to string" { + t.Error("expect err:(1, 1): Can't convert 1(int64) to string but got ", err) + } + +} + +func TestMarshalMixedTypeArray(t *testing.T) { + type InnerStruct struct { + IntField int + StrField string + } + + type TestStruct struct { + ArrayField []interface{} + } + + expected := []byte(`ArrayField = [3.14, 100, true, "hello world", { IntField = 100, StrField = "inner1" }, [{ IntField = 200, StrField = "inner2" }, { IntField = 300, StrField = "inner3" }]] +`) + + if result, err := Marshal(TestStruct{ + ArrayField: []interface{}{ + 3.14, + 100, + true, + "hello world", + InnerStruct{ + IntField: 100, + StrField: "inner1", + }, + []InnerStruct{ + {IntField: 200, StrField: "inner2"}, + {IntField: 300, StrField: "inner3"}, + }, + }, + }); err == nil { + if !bytes.Equal(result, expected) { + t.Errorf("Bad marshal: expected\n----\n%s\n----\ngot\n----\n%s\n----\n", expected, result) + } + } else { + t.Fatal(err) + } +} + +func TestUnmarshalMixedTypeArray(t *testing.T) { + type TestStruct struct { + ArrayField []interface{} + } + + toml := []byte(`ArrayField = [3.14,100,true,"hello world",{Field = "inner1"},[{Field = "inner2"},{Field = "inner3"}]] +`) + + actual := TestStruct{} + expected := TestStruct{ + ArrayField: []interface{}{ + 3.14, + int64(100), + true, + "hello world", + map[string]interface{}{ + "Field": "inner1", + }, + []map[string]interface{}{ + {"Field": "inner2"}, + {"Field": "inner3"}, + }, + }, + } + + if err := Unmarshal(toml, &actual); err == nil { + if !reflect.DeepEqual(actual, expected) { + t.Errorf("Bad unmarshal: expected %#v, got %#v", expected, actual) + } + } else { + t.Fatal(err) + } +} + +func TestUnmarshalArray(t *testing.T) { + var tree *Tree + var err error + + tree, _ = LoadBytes(sliceTomlDemo) + var actual1 arrayStruct + err = tree.Unmarshal(&actual1) + if err != nil { + t.Error("shound not err", err) + } + + tree, _ = TreeFromMap(tree.ToMap()) + var actual2 arrayStruct + err = tree.Unmarshal(&actual2) + if err != nil { + t.Error("shound not err", err) + } + + expected := arrayStruct{ + Slice: [4]string{"Howdy", "Hey There"}, + SlicePtr: &[4]string{"Howdy", "Hey There"}, + IntSlice: [4]int{1, 2}, + IntSlicePtr: &[4]int{1, 2}, + StructSlice: [4]basicMarshalTestSubStruct{{"1"}, {"2"}}, + StructSlicePtr: &[4]basicMarshalTestSubStruct{{"1"}, {"2"}}, + } + if !reflect.DeepEqual(actual1, expected) { + t.Errorf("Bad unmarshal: expected %v, got %v", expected, actual1) + } + if !reflect.DeepEqual(actual2, expected) { + t.Errorf("Bad unmarshal: expected %v, got %v", expected, actual2) + } +} + +func TestUnmarshalArrayFail(t *testing.T) { + tree, _ := TreeFromMap(map[string]interface{}{ + "str_slice": []string{"Howdy", "Hey There"}, + }) + + var actual arrayTooSmallStruct + err := tree.Unmarshal(&actual) + if err.Error() != "(0, 0): unmarshal: TOML array length (2) exceeds destination array length (1)" { + t.Error("expect err:(0, 0): unmarshal: TOML array length (2) exceeds destination array length (1) but got ", err) + } +} + +func TestUnmarshalArrayFail2(t *testing.T) { + tree, _ := Load(`str_slice=["Howdy","Hey There"]`) + + var actual arrayTooSmallStruct + err := tree.Unmarshal(&actual) + if err.Error() != "(1, 1): unmarshal: TOML array length (2) exceeds destination array length (1)" { + t.Error("expect err:(1, 1): unmarshal: TOML array length (2) exceeds destination array length (1) but got ", err) + } +} + +func TestUnmarshalArrayFail3(t *testing.T) { + tree, _ := Load(`[[struct_slice]] +String2="1" +[[struct_slice]] +String2="2"`) + + var actual arrayTooSmallStruct + err := tree.Unmarshal(&actual) + if err.Error() != "(3, 1): unmarshal: TOML array length (2) exceeds destination array length (1)" { + t.Error("expect err:(3, 1): unmarshal: TOML array length (2) exceeds destination array length (1) but got ", err) + } +} + +func TestDecoderStrict(t *testing.T) { + input := ` +[decoded] + key = "" + +[undecoded] + key = "" + + [undecoded.inner] + key = "" + + [[undecoded.array]] + key = "" + + [[undecoded.array]] + key = "" + +` + var doc struct { + Decoded struct { + Key string + } + } + + expected := `undecoded keys: ["undecoded.array.0.key" "undecoded.array.1.key" "undecoded.inner.key" "undecoded.key"]` + + err := NewDecoder(bytes.NewReader([]byte(input))).Strict(true).Decode(&doc) + if err == nil { + t.Error("expected error, got none") + } else if err.Error() != expected { + t.Errorf("expect err: %s, got: %s", expected, err.Error()) + } + + if err := NewDecoder(bytes.NewReader([]byte(input))).Decode(&doc); err != nil { + t.Errorf("unexpected err: %s", err) + } + + var m map[string]interface{} + if err := NewDecoder(bytes.NewReader([]byte(input))).Decode(&m); err != nil { + t.Errorf("unexpected err: %s", err) + } +} + +func TestDecoderStrictValid(t *testing.T) { + input := ` +[decoded] + key = "" +` + var doc struct { + Decoded struct { + Key string + } + } + + err := NewDecoder(bytes.NewReader([]byte(input))).Strict(true).Decode(&doc) + if err != nil { + t.Fatal("unexpected error:", err) + } +} + +type docUnmarshalTOML struct { + Decoded struct { + Key string + } +} + +func (d *docUnmarshalTOML) UnmarshalTOML(i interface{}) error { + if iMap, ok := i.(map[string]interface{}); !ok { + return fmt.Errorf("type assertion error: wants %T, have %T", map[string]interface{}{}, i) + } else if key, ok := iMap["key"]; !ok { + return fmt.Errorf("key '%s' not in map", "key") + } else if keyString, ok := key.(string); !ok { + return fmt.Errorf("type assertion error: wants %T, have %T", "", key) + } else { + d.Decoded.Key = keyString + } + return nil +} + +func TestDecoderStrictCustomUnmarshal(t *testing.T) { + input := `key = "ok"` + var doc docUnmarshalTOML + err := NewDecoder(bytes.NewReader([]byte(input))).Strict(true).Decode(&doc) + if err != nil { + t.Fatal("unexpected error:", err) + } + if doc.Decoded.Key != "ok" { + t.Errorf("Bad unmarshal: expected ok, got %v", doc.Decoded.Key) + } +} + +type parent struct { + Doc docUnmarshalTOML + DocPointer *docUnmarshalTOML +} + +func TestCustomUnmarshal(t *testing.T) { + input := ` +[Doc] + key = "ok1" +[DocPointer] + key = "ok2" +` + + var d parent + if err := Unmarshal([]byte(input), &d); err != nil { + t.Fatalf("unexpected err: %s", err.Error()) + } + if d.Doc.Decoded.Key != "ok1" { + t.Errorf("Bad unmarshal: expected ok, got %v", d.Doc.Decoded.Key) + } + if d.DocPointer.Decoded.Key != "ok2" { + t.Errorf("Bad unmarshal: expected ok, got %v", d.DocPointer.Decoded.Key) + } +} + +func TestCustomUnmarshalError(t *testing.T) { + input := ` +[Doc] + key = 1 +[DocPointer] + key = "ok2" +` + + expected := "(2, 1): unmarshal toml: type assertion error: wants string, have int64" + + var d parent + err := Unmarshal([]byte(input), &d) + if err == nil { + t.Error("expected error, got none") + } else if err.Error() != expected { + t.Errorf("expect err: %s, got: %s", expected, err.Error()) + } +} + +type intWrapper struct { + Value int +} + +func (w *intWrapper) UnmarshalText(text []byte) error { + var err error + if w.Value, err = strconv.Atoi(string(text)); err == nil { + return nil + } + if b, err := strconv.ParseBool(string(text)); err == nil { + if b { + w.Value = 1 + } + return nil + } + if f, err := strconv.ParseFloat(string(text), 32); err == nil { + w.Value = int(f) + return nil + } + return fmt.Errorf("unsupported: %s", text) +} + +func TestTextUnmarshal(t *testing.T) { + var doc struct { + UnixTime intWrapper + Version *intWrapper + + Bool intWrapper + Int intWrapper + Float intWrapper + } + + input := ` +UnixTime = "12" +Version = "42" +Bool = true +Int = 21 +Float = 2.0 +` + + if err := Unmarshal([]byte(input), &doc); err != nil { + t.Fatalf("unexpected err: %s", err.Error()) + } + if doc.UnixTime.Value != 12 { + t.Fatalf("expected UnixTime: 12 got: %d", doc.UnixTime.Value) + } + if doc.Version.Value != 42 { + t.Fatalf("expected Version: 42 got: %d", doc.Version.Value) + } + if doc.Bool.Value != 1 { + t.Fatalf("expected Bool: 1 got: %d", doc.Bool.Value) + } + if doc.Int.Value != 21 { + t.Fatalf("expected Int: 21 got: %d", doc.Int.Value) + } + if doc.Float.Value != 2 { + t.Fatalf("expected Float: 2 got: %d", doc.Float.Value) + } +} + +func TestTextUnmarshalError(t *testing.T) { + var doc struct { + Failer intWrapper + } + + input := `Failer = "hello"` + if err := Unmarshal([]byte(input), &doc); err == nil { + t.Fatalf("expected err, got none") + } +} + +// issue406 +func TestPreserveNotEmptyField(t *testing.T) { + toml := []byte(`Field1 = "ccc"`) + type Inner struct { + InnerField1 string + InnerField2 int + } + type TestStruct struct { + Field1 string + Field2 int + Field3 Inner + } + + actual := TestStruct{ + "aaa", + 100, + Inner{ + "bbb", + 200, + }, + } + + expected := TestStruct{ + "ccc", + 100, + Inner{ + "bbb", + 200, + }, + } + + err := Unmarshal(toml, &actual) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(actual, expected) { + t.Errorf("Bad unmarshal: expected %+v, got %+v", expected, actual) + } +} + +// github issue 432 +func TestUnmarshalEmptyInterface(t *testing.T) { + doc := []byte(`User = "pelletier"`) + + var v interface{} + + err := Unmarshal(doc, &v) + if err != nil { + t.Fatal(err) + } + + x, ok := v.(map[string]interface{}) + if !ok { + t.Fatal(err) + } + + if x["User"] != "pelletier" { + t.Fatalf("expected User=pelletier, but got %v", x) + } +} + +func TestUnmarshalEmptyInterfaceDeep(t *testing.T) { + doc := []byte(` +User = "pelletier" +Age = 99 + +[foo] +bar = 42 +`) + + var v interface{} + + err := Unmarshal(doc, &v) + if err != nil { + t.Fatal(err) + } + + x, ok := v.(map[string]interface{}) + if !ok { + t.Fatal(err) + } + + expected := map[string]interface{}{ + "User": "pelletier", + "Age": 99, + "foo": map[string]interface{}{ + "bar": 42, + }, + } + + reflect.DeepEqual(x, expected) +} + +type Config struct { + Key string `toml:"key"` + Obj Custom `toml:"obj"` +} + +type Custom struct { + v string +} + +func (c *Custom) UnmarshalTOML(v interface{}) error { + c.v = "called" + return nil +} + +func TestGithubIssue431(t *testing.T) { + doc := `key = "value"` + tree, err := LoadBytes([]byte(doc)) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + var c Config + if err := tree.Unmarshal(&c); err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if c.Key != "value" { + t.Errorf("expected c.Key='value', not '%s'", c.Key) + } + + if c.Obj.v == "called" { + t.Errorf("UnmarshalTOML should not have been called") + } +} diff -Nru golang-github-pelletier-go-toml-1.6.0/parser.go golang-github-pelletier-go-toml-1.8.1/parser.go --- golang-github-pelletier-go-toml-1.6.0/parser.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/parser.go 2020-09-12 18:42:04.000000000 +0000 @@ -158,6 +158,11 @@ if err := p.tree.createSubTree(keys, startToken.Position); err != nil { p.raiseError(key, "%s", err) } + destTree := p.tree.GetPath(keys) + if target, ok := destTree.(*Tree); ok && target != nil && target.inline { + p.raiseError(key, "could not re-define exist inline table or its sub-table : %s", + strings.Join(keys, ".")) + } p.assume(tokenRightBracket) p.currentTable = keys return p.parseStart @@ -201,6 +206,11 @@ strings.Join(tableKey, ".")) } + if targetNode.inline { + p.raiseError(key, "could not add key or sub-table to exist inline table or its sub-table : %s", + strings.Join(tableKey, ".")) + } + // assign value to the found table keyVal := parsedKey[len(parsedKey)-1] localKey := []string{keyVal} @@ -411,12 +421,13 @@ if tokenIsComma(previous) { p.raiseError(previous, "trailing comma at the end of inline table") } + tree.inline = true return tree } func (p *tomlParser) parseArray() interface{} { var array []interface{} - arrayType := reflect.TypeOf(nil) + arrayType := reflect.TypeOf(newTree()) for { follow := p.peek() if follow == nil || follow.typ == tokenEOF { @@ -427,11 +438,8 @@ break } val := p.parseRvalue() - if arrayType == nil { - arrayType = reflect.TypeOf(val) - } if reflect.TypeOf(val) != arrayType { - p.raiseError(follow, "mixed types in array") + arrayType = nil } array = append(array, val) follow = p.peek() @@ -445,6 +453,12 @@ p.getToken() } } + + // if the array is a mixed-type array or its length is 0, + // don't convert it to a table array + if len(array) <= 0 { + arrayType = nil + } // An array of Trees is actually an array of inline // tables, which is a shorthand for a table array. If the // array was not converted from []interface{} to []*Tree, diff -Nru golang-github-pelletier-go-toml-1.6.0/parser_test.go golang-github-pelletier-go-toml-1.8.1/parser_test.go --- golang-github-pelletier-go-toml-1.6.0/parser_test.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/parser_test.go 2020-09-12 18:42:04.000000000 +0000 @@ -239,7 +239,8 @@ Minute: 32, Second: 0, Nanosecond: 0, - }}, + }, + }, }) } @@ -257,7 +258,8 @@ Minute: 32, Second: 0, Nanosecond: 999999000, - }}, + }, + }, }) } @@ -486,18 +488,6 @@ }) } -func TestArrayMixedTypes(t *testing.T) { - _, err := Load("a = [42, 16.0]") - if err.Error() != "(1, 10): mixed types in array" { - t.Error("Bad error message:", err.Error()) - } - - _, err = Load("a = [42, \"hello\"]") - if err.Error() != "(1, 11): mixed types in array" { - t.Error("Bad error message:", err.Error()) - } -} - func TestArrayNestedStrings(t *testing.T) { tree, err := Load("data = [ [\"gamma\", \"delta\"], [\"Foo\"] ]") assertTree(t, tree, err, map[string]interface{}{ @@ -581,6 +571,39 @@ }) } +func TestNestedInlineGroup(t *testing.T) { + tree, err := Load("out = {block0 = {x = 99, y = 100}, block1 = {p = \"999\", q = \"1000\"}}") + assertTree(t, tree, err, map[string]interface{}{ + "out": map[string]interface{}{ + "block0": map[string]interface{}{ + "x": int64(99), + "y": int64(100), + }, + "block1": map[string]interface{}{ + "p": "999", + "q": "1000", + }, + }, + }) +} + +func TestArrayInNestedInlineGroup(t *testing.T) { + tree, err := Load(`image = {name = "xxx", palette = {id = 100, colors = ["red", "blue", "green"]}}`) + assertTree(t, tree, err, map[string]interface{}{ + "image": map[string]interface{}{ + "name": "xxx", + "palette": map[string]interface{}{ + "id": int64(100), + "colors": []string{ + "red", + "blue", + "green", + }, + }, + }, + }) +} + func TestExampleInlineGroup(t *testing.T) { tree, err := Load(`name = { first = "Tom", last = "Preston-Werner" } point = { x = 1, y = 2 }`) @@ -644,7 +667,7 @@ func TestInlineTableCommaExpected(t *testing.T) { _, err := Load("foo = {hello = 53 test = foo}") - if err.Error() != "(1, 19): comma expected between fields in inline table" { + if err.Error() != "(1, 19): unexpected token type in inline table: no value can start with t" { t.Error("Bad error message:", err.Error()) } } @@ -658,7 +681,42 @@ func TestInlineTableDoubleComma(t *testing.T) { _, err := Load("foo = {hello = 53,, foo = 17}") - if err.Error() != "(1, 19): need field between two commas in inline table" { + if err.Error() != "(1, 19): unexpected token type in inline table: keys cannot contain , character" { + t.Error("Bad error message:", err.Error()) + } +} + +func TestInlineTableTrailingComma(t *testing.T) { + _, err := Load("foo = {hello = 53, foo = 17,}") + if err.Error() != "(1, 28): trailing comma at the end of inline table" { + t.Error("Bad error message:", err.Error()) + } +} + +func TestAddKeyToInlineTable(t *testing.T) { + _, err := Load("type = { name = \"Nail\" }\ntype.edible = false") + if err.Error() != "(2, 1): could not add key or sub-table to exist inline table or its sub-table : type" { + t.Error("Bad error message:", err.Error()) + } +} + +func TestAddSubTableToInlineTable(t *testing.T) { + _, err := Load("a = { b = \"c\" }\na.d.e = \"f\"") + if err.Error() != "(2, 1): could not add key or sub-table to exist inline table or its sub-table : a.d" { + t.Error("Bad error message:", err.Error()) + } +} + +func TestAddKeyToSubTableOfInlineTable(t *testing.T) { + _, err := Load("a = { b = { c = \"d\" } }\na.b.e = \"f\"") + if err.Error() != "(2, 1): could not add key or sub-table to exist inline table or its sub-table : a.b" { + t.Error("Bad error message:", err.Error()) + } +} + +func TestReDefineInlineTable(t *testing.T) { + _, err := Load("a = { b = \"c\" }\n[a]\n d = \"e\"") + if err.Error() != "(2, 2): could not re-define exist inline table or its sub-table : a" { t.Error("Bad error message:", err.Error()) } } @@ -750,6 +808,7 @@ []string{"gamma", "delta"}, []int64{1, 2}, }, + "score": 4e-08, }, }) } @@ -786,6 +845,7 @@ []string{"gamma", "delta"}, []int64{1, 2}, }, + "score": 4e-08, }, }) } @@ -858,13 +918,11 @@ {"hello world", "\"hello world\""}, {"\b\t\n\f\r\"\\", "\"\\b\\t\\n\\f\\r\\\"\\\\\""}, {"\x05", "\"\\u0005\""}, - {time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC), - "1979-05-27T07:32:00Z"}, - {[]interface{}{"gamma", "delta"}, - "[\"gamma\",\"delta\"]"}, + {time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC), "1979-05-27T07:32:00Z"}, + {[]interface{}{"gamma", "delta"}, "[\"gamma\", \"delta\"]"}, {nil, ""}, } { - result, err := tomlValueStringRepresentation(item.Value, "", false) + result, err := tomlValueStringRepresentation(item.Value, "", "", OrderAlphabetical, false) if err != nil { t.Errorf("Test %d - unexpected error: %s", idx, err) } @@ -991,7 +1049,7 @@ } _, err = Load("a=_1_2") - if err.Error() != "(1, 3): cannot start number with underscore" { + if err.Error() != "(1, 3): no value can start with _" { t.Error("Bad error message:", err.Error()) } } @@ -1055,11 +1113,10 @@ the lazy dog.""" str3 = """\ - The quick brown \ - fox jumps over \ - the lazy dog.\ - """`) - + The quick brown \` + " " + ` + fox jumps over \` + " " + ` + the lazy dog.\` + " " + ` + """`) if err != nil { t.Fatalf("unexpected error: %v", err) } diff -Nru golang-github-pelletier-go-toml-1.6.0/query/doc.go golang-github-pelletier-go-toml-1.8.1/query/doc.go --- golang-github-pelletier-go-toml-1.6.0/query/doc.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/query/doc.go 2020-09-12 18:42:04.000000000 +0000 @@ -25,7 +25,7 @@ // items. // // As illustrated above, the query path is much more efficient, especially since -// the structure of the TOML file can vary. Rather than making assumptions about +// the structure of the TOML file can vary. Rather than making assumptions about // a document's structure, a query allows the programmer to make structured // requests into the document, and get zero or more values as a result. // @@ -35,7 +35,7 @@ // sub-expressions: // // $ -// Root of the TOML tree. This must always come first. +// Root of the TOML tree. This must always come first. // .name // Selects child of this node, where 'name' is a TOML key // name. @@ -57,7 +57,7 @@ // sub-expressions: index, key name, or filter. // [start:end:step] // Slice operator - selects array elements from start to -// end-1, at the given step. All three arguments are +// end-1, at the given step. All three arguments are // optional. // [?(filter)] // Named filter expression - the function 'filter' is @@ -80,25 +80,23 @@ // Slice expressions also allow negative indexes for the start and stop // arguments. // -// // select all array elements. +// // select all array elements except the last one. // query.CompileAndExecute("$.foo[0:-1]", tree) // // Slice expressions may have an optional stride/step parameter: // // // select every other element -// query.CompileAndExecute("$.foo[0:-1:2]", tree) +// query.CompileAndExecute("$.foo[0::2]", tree) // // Slice start and end parameters are also optional: // // // these are all equivalent and select all the values in the array // query.CompileAndExecute("$.foo[:]", tree) -// query.CompileAndExecute("$.foo[0:]", tree) -// query.CompileAndExecute("$.foo[:-1]", tree) -// query.CompileAndExecute("$.foo[0:-1:]", tree) +// query.CompileAndExecute("$.foo[::]", tree) // query.CompileAndExecute("$.foo[::1]", tree) +// query.CompileAndExecute("$.foo[0:]", tree) +// query.CompileAndExecute("$.foo[0::]", tree) // query.CompileAndExecute("$.foo[0::1]", tree) -// query.CompileAndExecute("$.foo[:-1:1]", tree) -// query.CompileAndExecute("$.foo[0:-1:1]", tree) // // Query Filters // @@ -126,8 +124,8 @@ // // Query Results // -// An executed query returns a Result object. This contains the nodes -// in the TOML tree that qualify the query expression. Position information +// An executed query returns a Result object. This contains the nodes +// in the TOML tree that qualify the query expression. Position information // is also available for each value in the set. // // // display the results of a query @@ -139,7 +137,7 @@ // Compiled Queries // // Queries may be executed directly on a Tree object, or compiled ahead -// of time and executed discretely. The former is more convenient, but has the +// of time and executed discretely. The former is more convenient, but has the // penalty of having to recompile the query expression each time. // // // basic query @@ -155,7 +153,7 @@ // User Defined Query Filters // // Filter expressions may also be user defined by using the SetFilter() -// function on the Query object. The function must return true/false, which +// function on the Query object. The function must return true/false, which // signifies if the passed node is kept or discarded, respectively. // // // create a query that references a user-defined filter @@ -166,7 +164,7 @@ // if tree, ok := node.(*Tree); ok { // return tree.Has("baz") // } -// return false // reject all other node types +// return false // reject all other node types // }) // // // run the query diff -Nru golang-github-pelletier-go-toml-1.6.0/query/match.go golang-github-pelletier-go-toml-1.8.1/query/match.go --- golang-github-pelletier-go-toml-1.6.0/query/match.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/query/match.go 2020-09-12 18:42:04.000000000 +0000 @@ -2,6 +2,8 @@ import ( "fmt" + "reflect" + "github.com/pelletier/go-toml" ) @@ -44,16 +46,16 @@ func (f *matchKeyFn) call(node interface{}, ctx *queryContext) { if array, ok := node.([]*toml.Tree); ok { for _, tree := range array { - item := tree.Get(f.Name) + item := tree.GetPath([]string{f.Name}) if item != nil { - ctx.lastPosition = tree.GetPosition(f.Name) + ctx.lastPosition = tree.GetPositionPath([]string{f.Name}) f.next.call(item, ctx) } } } else if tree, ok := node.(*toml.Tree); ok { - item := tree.Get(f.Name) + item := tree.GetPath([]string{f.Name}) if item != nil { - ctx.lastPosition = tree.GetPosition(f.Name) + ctx.lastPosition = tree.GetPositionPath([]string{f.Name}) f.next.call(item, ctx) } } @@ -70,51 +72,128 @@ } func (f *matchIndexFn) call(node interface{}, ctx *queryContext) { - if arr, ok := node.([]interface{}); ok { - if f.Idx < len(arr) && f.Idx >= 0 { - if treesArray, ok := node.([]*toml.Tree); ok { - if len(treesArray) > 0 { - ctx.lastPosition = treesArray[0].Position() - } - } - f.next.call(arr[f.Idx], ctx) + v := reflect.ValueOf(node) + if v.Kind() == reflect.Slice { + if v.Len() == 0 { + return + } + + // Manage negative values + idx := f.Idx + if idx < 0 { + idx += v.Len() } + if 0 <= idx && idx < v.Len() { + callNextIndexSlice(f.next, node, ctx, v.Index(idx).Interface()) + } + } +} + +func callNextIndexSlice(next pathFn, node interface{}, ctx *queryContext, value interface{}) { + if treesArray, ok := node.([]*toml.Tree); ok { + ctx.lastPosition = treesArray[0].Position() } + next.call(value, ctx) } // filter by slicing type matchSliceFn struct { matchBase - Start, End, Step int + Start, End, Step *int } -func newMatchSliceFn(start, end, step int) *matchSliceFn { - return &matchSliceFn{Start: start, End: end, Step: step} +func newMatchSliceFn() *matchSliceFn { + return &matchSliceFn{} +} + +func (f *matchSliceFn) setStart(start int) *matchSliceFn { + f.Start = &start + return f +} + +func (f *matchSliceFn) setEnd(end int) *matchSliceFn { + f.End = &end + return f +} + +func (f *matchSliceFn) setStep(step int) *matchSliceFn { + f.Step = &step + return f } func (f *matchSliceFn) call(node interface{}, ctx *queryContext) { - if arr, ok := node.([]interface{}); ok { - // adjust indexes for negative values, reverse ordering - realStart, realEnd := f.Start, f.End - if realStart < 0 { - realStart = len(arr) + realStart - } - if realEnd < 0 { - realEnd = len(arr) + realEnd - } - if realEnd < realStart { - realEnd, realStart = realStart, realEnd // swap - } - // loop and gather - for idx := realStart; idx < realEnd; idx += f.Step { - if treesArray, ok := node.([]*toml.Tree); ok { - if len(treesArray) > 0 { - ctx.lastPosition = treesArray[0].Position() - } + v := reflect.ValueOf(node) + if v.Kind() == reflect.Slice { + if v.Len() == 0 { + return + } + + var start, end, step int + + // Initialize step + if f.Step != nil { + step = *f.Step + } else { + step = 1 + } + + // Initialize start + if f.Start != nil { + start = *f.Start + // Manage negative values + if start < 0 { + start += v.Len() } - f.next.call(arr[idx], ctx) + // Manage out of range values + start = max(start, 0) + start = min(start, v.Len()-1) + } else if step > 0 { + start = 0 + } else { + start = v.Len() - 1 } + + // Initialize end + if f.End != nil { + end = *f.End + // Manage negative values + if end < 0 { + end += v.Len() + } + // Manage out of range values + end = max(end, -1) + end = min(end, v.Len()) + } else if step > 0 { + end = v.Len() + } else { + end = -1 + } + + // Loop on values + if step > 0 { + for idx := start; idx < end; idx += step { + callNextIndexSlice(f.next, node, ctx, v.Index(idx).Interface()) + } + } else { + for idx := start; idx > end; idx += step { + callNextIndexSlice(f.next, node, ctx, v.Index(idx).Interface()) + } + } + } +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a } + return b } // match anything @@ -129,8 +208,8 @@ func (f *matchAnyFn) call(node interface{}, ctx *queryContext) { if tree, ok := node.(*toml.Tree); ok { for _, k := range tree.Keys() { - v := tree.Get(k) - ctx.lastPosition = tree.GetPosition(k) + v := tree.GetPath([]string{k}) + ctx.lastPosition = tree.GetPositionPath([]string{k}) f.next.call(v, ctx) } } @@ -168,8 +247,8 @@ var visit func(tree *toml.Tree) visit = func(tree *toml.Tree) { for _, k := range tree.Keys() { - v := tree.Get(k) - ctx.lastPosition = tree.GetPosition(k) + v := tree.GetPath([]string{k}) + ctx.lastPosition = tree.GetPositionPath([]string{k}) f.next.call(v, ctx) switch node := v.(type) { case *toml.Tree: @@ -207,9 +286,9 @@ switch castNode := node.(type) { case *toml.Tree: for _, k := range castNode.Keys() { - v := castNode.Get(k) + v := castNode.GetPath([]string{k}) if fn(v) { - ctx.lastPosition = castNode.GetPosition(k) + ctx.lastPosition = castNode.GetPositionPath([]string{k}) f.next.call(v, ctx) } } diff -Nru golang-github-pelletier-go-toml-1.6.0/query/match_test.go golang-github-pelletier-go-toml-1.8.1/query/match_test.go --- golang-github-pelletier-go-toml-1.6.0/query/match_test.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/query/match_test.go 2020-09-12 18:42:04.000000000 +0000 @@ -2,8 +2,10 @@ import ( "fmt" - "github.com/pelletier/go-toml" + "strconv" "testing" + + "github.com/pelletier/go-toml" ) // dump path tree to a string @@ -19,8 +21,17 @@ result += fmt.Sprintf("{%d}", fn.Idx) result += pathString(fn.next) case *matchSliceFn: - result += fmt.Sprintf("{%d:%d:%d}", - fn.Start, fn.End, fn.Step) + startString, endString, stepString := "nil", "nil", "nil" + if fn.Start != nil { + startString = strconv.Itoa(*fn.Start) + } + if fn.End != nil { + endString = strconv.Itoa(*fn.End) + } + if fn.Step != nil { + stepString = strconv.Itoa(*fn.Step) + } + result += fmt.Sprintf("{%s:%s:%s}", startString, endString, stepString) result += pathString(fn.next) case *matchAnyFn: result += "{}" @@ -110,7 +121,7 @@ assertPath(t, "$[123:]", buildPath( - newMatchSliceFn(123, maxInt, 1), + newMatchSliceFn().setStart(123), )) } @@ -118,7 +129,7 @@ assertPath(t, "$[123:456]", buildPath( - newMatchSliceFn(123, 456, 1), + newMatchSliceFn().setStart(123).setEnd(456), )) } @@ -126,7 +137,7 @@ assertPath(t, "$[123:456:]", buildPath( - newMatchSliceFn(123, 456, 1), + newMatchSliceFn().setStart(123).setEnd(456), )) } @@ -134,7 +145,7 @@ assertPath(t, "$[123::7]", buildPath( - newMatchSliceFn(123, maxInt, 7), + newMatchSliceFn().setStart(123).setStep(7), )) } @@ -142,7 +153,7 @@ assertPath(t, "$[:456:7]", buildPath( - newMatchSliceFn(0, 456, 7), + newMatchSliceFn().setEnd(456).setStep(7), )) } @@ -150,7 +161,7 @@ assertPath(t, "$[::7]", buildPath( - newMatchSliceFn(0, maxInt, 7), + newMatchSliceFn().setStep(7), )) } @@ -158,7 +169,7 @@ assertPath(t, "$[123:456:7]", buildPath( - newMatchSliceFn(123, 456, 7), + newMatchSliceFn().setStart(123).setEnd(456).setStep(7), )) } diff -Nru golang-github-pelletier-go-toml-1.6.0/query/parser.go golang-github-pelletier-go-toml-1.8.1/query/parser.go --- golang-github-pelletier-go-toml-1.6.0/query/parser.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/query/parser.go 2020-09-12 18:42:04.000000000 +0000 @@ -203,12 +203,13 @@ func (p *queryParser) parseSliceExpr() queryParserStateFn { // init slice to grab all elements - start, end, step := 0, maxInt, 1 + var start, end, step *int = nil, nil, nil // parse optional start tok := p.getToken() if tok.typ == tokenInteger { - start = tok.Int() + v := tok.Int() + start = &v tok = p.getToken() } if tok.typ != tokenColon { @@ -218,11 +219,12 @@ // parse optional end tok = p.getToken() if tok.typ == tokenInteger { - end = tok.Int() + v := tok.Int() + end = &v tok = p.getToken() } if tok.typ == tokenRightBracket { - p.query.appendPath(newMatchSliceFn(start, end, step)) + p.query.appendPath(&matchSliceFn{Start: start, End: end, Step: step}) return p.parseMatchExpr } if tok.typ != tokenColon { @@ -232,17 +234,18 @@ // parse optional step tok = p.getToken() if tok.typ == tokenInteger { - step = tok.Int() - if step < 0 { - return p.parseError(tok, "step must be a positive value") + v := tok.Int() + if v == 0 { + return p.parseError(tok, "step cannot be zero") } + step = &v tok = p.getToken() } if tok.typ != tokenRightBracket { return p.parseError(tok, "expected ']'") } - p.query.appendPath(newMatchSliceFn(start, end, step)) + p.query.appendPath(&matchSliceFn{Start: start, End: end, Step: step}) return p.parseMatchExpr } diff -Nru golang-github-pelletier-go-toml-1.6.0/query/parser_test.go golang-github-pelletier-go-toml-1.8.1/query/parser_test.go --- golang-github-pelletier-go-toml-1.6.0/query/parser_test.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/query/parser_test.go 2020-09-12 18:42:04.000000000 +0000 @@ -78,6 +78,19 @@ } } +func assertParseError(t *testing.T, query string, errString string) { + _, err := Compile(query) + if err == nil { + t.Error("error should be non-nil") + return + } + if err.Error() != errString { + t.Errorf("error does not match") + t.Log("test:", err.Error()) + t.Log("ref: ", errString) + } +} + func assertQueryPositions(t *testing.T, tomlDoc string, query string, ref []interface{}) { tree, err := toml.Load(tomlDoc) if err != nil { @@ -128,54 +141,213 @@ }) } -func TestQueryIndex(t *testing.T) { +func TestQueryKeyUnicodeString(t *testing.T) { assertQueryPositions(t, - "[foo]\na = [1,2,3,4,5,6,7,8,9,0]", - "$.foo.a[5]", + "['f𝟘.o']\na = 42", + "$['f𝟘.o']['a']", []interface{}{ queryTestNode{ - int64(6), toml.Position{2, 1}, + int64(42), toml.Position{2, 1}, }, }) } +func TestQueryIndexError1(t *testing.T) { + assertParseError(t, "$.foo.a[5", "(1, 10): expected ',' or ']', not ''") +} + +func TestQueryIndexError2(t *testing.T) { + assertParseError(t, "$.foo.a[]", "(1, 9): expected union sub expression, not ']', 0") +} + +func TestQueryIndex(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[5]", + []interface{}{ + queryTestNode{int64(5), toml.Position{2, 1}}, + }) +} + +func TestQueryIndexNegative(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[-2]", + []interface{}{ + queryTestNode{int64(8), toml.Position{2, 1}}, + }) +} + +func TestQueryIndexWrong(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[99]", + []interface{}{}) +} + +func TestQueryIndexEmpty(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = []", + "$.foo.a[5]", + []interface{}{}) +} + +func TestQueryIndexTree(t *testing.T) { + assertQueryPositions(t, + "[[foo]]\na = [0,1,2,3,4,5,6,7,8,9]\n[[foo]]\nb = 3", + "$.foo[1].b", + []interface{}{ + queryTestNode{int64(3), toml.Position{4, 1}}, + }) +} + +func TestQuerySliceError1(t *testing.T) { + assertParseError(t, "$.foo.a[3:?]", "(1, 11): expected ']' or ':'") +} + +func TestQuerySliceError2(t *testing.T) { + assertParseError(t, "$.foo.a[:::]", "(1, 11): expected ']'") +} + +func TestQuerySliceError3(t *testing.T) { + assertParseError(t, "$.foo.a[::0]", "(1, 11): step cannot be zero") +} + func TestQuerySliceRange(t *testing.T) { assertQueryPositions(t, - "[foo]\na = [1,2,3,4,5,6,7,8,9,0]", - "$.foo.a[0:5]", + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[:5]", []interface{}{ - queryTestNode{ - int64(1), toml.Position{2, 1}, - }, - queryTestNode{ - int64(2), toml.Position{2, 1}, - }, - queryTestNode{ - int64(3), toml.Position{2, 1}, - }, - queryTestNode{ - int64(4), toml.Position{2, 1}, - }, - queryTestNode{ - int64(5), toml.Position{2, 1}, - }, + queryTestNode{int64(0), toml.Position{2, 1}}, + queryTestNode{int64(1), toml.Position{2, 1}}, + queryTestNode{int64(2), toml.Position{2, 1}}, + queryTestNode{int64(3), toml.Position{2, 1}}, + queryTestNode{int64(4), toml.Position{2, 1}}, }) } func TestQuerySliceStep(t *testing.T) { assertQueryPositions(t, - "[foo]\na = [1,2,3,4,5,6,7,8,9,0]", + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", "$.foo.a[0:5:2]", []interface{}{ + queryTestNode{int64(0), toml.Position{2, 1}}, + queryTestNode{int64(2), toml.Position{2, 1}}, + queryTestNode{int64(4), toml.Position{2, 1}}, + }) +} + +func TestQuerySliceStartNegative(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[-3:]", + []interface{}{ + queryTestNode{int64(7), toml.Position{2, 1}}, + queryTestNode{int64(8), toml.Position{2, 1}}, + queryTestNode{int64(9), toml.Position{2, 1}}, + }) +} + +func TestQuerySliceEndNegative(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[:-6]", + []interface{}{ + queryTestNode{int64(0), toml.Position{2, 1}}, + queryTestNode{int64(1), toml.Position{2, 1}}, + queryTestNode{int64(2), toml.Position{2, 1}}, + queryTestNode{int64(3), toml.Position{2, 1}}, + }) +} + +func TestQuerySliceStepNegative(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[::-2]", + []interface{}{ + queryTestNode{int64(9), toml.Position{2, 1}}, + queryTestNode{int64(7), toml.Position{2, 1}}, + queryTestNode{int64(5), toml.Position{2, 1}}, + queryTestNode{int64(3), toml.Position{2, 1}}, + queryTestNode{int64(1), toml.Position{2, 1}}, + }) +} + +func TestQuerySliceStartOverRange(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[-99:3]", + []interface{}{ + queryTestNode{int64(0), toml.Position{2, 1}}, + queryTestNode{int64(1), toml.Position{2, 1}}, + queryTestNode{int64(2), toml.Position{2, 1}}, + }) +} + +func TestQuerySliceStartOverRangeNegative(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[99:7:-1]", + []interface{}{ + queryTestNode{int64(9), toml.Position{2, 1}}, + queryTestNode{int64(8), toml.Position{2, 1}}, + }) +} + +func TestQuerySliceEndOverRange(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[7:99]", + []interface{}{ + queryTestNode{int64(7), toml.Position{2, 1}}, + queryTestNode{int64(8), toml.Position{2, 1}}, + queryTestNode{int64(9), toml.Position{2, 1}}, + }) +} + +func TestQuerySliceEndOverRangeNegative(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[2:-99:-1]", + []interface{}{ + queryTestNode{int64(2), toml.Position{2, 1}}, + queryTestNode{int64(1), toml.Position{2, 1}}, + queryTestNode{int64(0), toml.Position{2, 1}}, + }) +} + +func TestQuerySliceWrongRange(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[5:3]", + []interface{}{}) +} + +func TestQuerySliceWrongRangeNegative(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = [0,1,2,3,4,5,6,7,8,9]", + "$.foo.a[3:5:-1]", + []interface{}{}) +} + +func TestQuerySliceEmpty(t *testing.T) { + assertQueryPositions(t, + "[foo]\na = []", + "$.foo.a[5:]", + []interface{}{}) +} + +func TestQuerySliceTree(t *testing.T) { + assertQueryPositions(t, + "[[foo]]\na='nok'\n[[foo]]\na = [0,1,2,3,4,5,6,7,8,9]\n[[foo]]\na='ok'\nb = 3", + "$.foo[1:].a", + []interface{}{ queryTestNode{ - int64(1), toml.Position{2, 1}, - }, - queryTestNode{ - int64(3), toml.Position{2, 1}, - }, - queryTestNode{ - int64(5), toml.Position{2, 1}, - }, + []interface{}{ + int64(0), int64(1), int64(2), int64(3), int64(4), + int64(5), int64(6), int64(7), int64(8), int64(9)}, + toml.Position{4, 1}}, + queryTestNode{"ok", toml.Position{6, 1}}, }) } @@ -265,12 +437,8 @@ "b": int64(2), }, toml.Position{1, 1}, }, - queryTestNode{ - int64(1), toml.Position{2, 1}, - }, - queryTestNode{ - int64(2), toml.Position{3, 1}, - }, + queryTestNode{int64(1), toml.Position{2, 1}}, + queryTestNode{int64(2), toml.Position{3, 1}}, queryTestNode{ map[string]interface{}{ "foo": map[string]interface{}{ @@ -285,12 +453,8 @@ "b": int64(4), }, toml.Position{4, 1}, }, - queryTestNode{ - int64(3), toml.Position{5, 1}, - }, - queryTestNode{ - int64(4), toml.Position{6, 1}, - }, + queryTestNode{int64(3), toml.Position{5, 1}}, + queryTestNode{int64(4), toml.Position{6, 1}}, queryTestNode{ map[string]interface{}{ "foo": map[string]interface{}{ @@ -305,12 +469,8 @@ "b": int64(6), }, toml.Position{7, 1}, }, - queryTestNode{ - int64(5), toml.Position{8, 1}, - }, - queryTestNode{ - int64(6), toml.Position{9, 1}, - }, + queryTestNode{int64(5), toml.Position{8, 1}}, + queryTestNode{int64(6), toml.Position{9, 1}}, }) } @@ -358,56 +518,30 @@ assertQueryPositions(t, string(buff), "$..[?(int)]", []interface{}{ - queryTestNode{ - int64(8001), toml.Position{13, 1}, - }, - queryTestNode{ - int64(8001), toml.Position{13, 1}, - }, - queryTestNode{ - int64(8002), toml.Position{13, 1}, - }, - queryTestNode{ - int64(5000), toml.Position{14, 1}, - }, + queryTestNode{int64(8001), toml.Position{13, 1}}, + queryTestNode{int64(8001), toml.Position{13, 1}}, + queryTestNode{int64(8002), toml.Position{13, 1}}, + queryTestNode{int64(5000), toml.Position{14, 1}}, }) assertQueryPositions(t, string(buff), "$..[?(string)]", []interface{}{ - queryTestNode{ - "TOML Example", toml.Position{3, 1}, - }, - queryTestNode{ - "Tom Preston-Werner", toml.Position{6, 1}, - }, - queryTestNode{ - "GitHub", toml.Position{7, 1}, - }, - queryTestNode{ - "GitHub Cofounder & CEO\nLikes tater tots and beer.", - toml.Position{8, 1}, - }, - queryTestNode{ - "192.168.1.1", toml.Position{12, 1}, - }, - queryTestNode{ - "10.0.0.1", toml.Position{21, 3}, - }, - queryTestNode{ - "eqdc10", toml.Position{22, 3}, - }, - queryTestNode{ - "10.0.0.2", toml.Position{25, 3}, - }, - queryTestNode{ - "eqdc10", toml.Position{26, 3}, - }, + queryTestNode{"TOML Example", toml.Position{3, 1}}, + queryTestNode{"Tom Preston-Werner", toml.Position{6, 1}}, + queryTestNode{"GitHub", toml.Position{7, 1}}, + queryTestNode{"GitHub Cofounder & CEO\nLikes tater tots and beer.", toml.Position{8, 1}}, + queryTestNode{"192.168.1.1", toml.Position{12, 1}}, + queryTestNode{"10.0.0.1", toml.Position{21, 3}}, + queryTestNode{"eqdc10", toml.Position{22, 3}}, + queryTestNode{"10.0.0.2", toml.Position{25, 3}}, + queryTestNode{"eqdc10", toml.Position{26, 3}}, }) assertQueryPositions(t, string(buff), "$..[?(float)]", - []interface{}{ // no float values in document + []interface{}{ + queryTestNode{4e-08, toml.Position{30, 1}}, }) tv, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z") @@ -460,6 +594,7 @@ []interface{}{"gamma", "delta"}, []interface{}{int64(1), int64(2)}, }, + "score": 4e-08, }, toml.Position{28, 1}, }, }) @@ -467,16 +602,12 @@ assertQueryPositions(t, string(buff), "$..[?(time)]", []interface{}{ - queryTestNode{ - tv, toml.Position{9, 1}, - }, + queryTestNode{tv, toml.Position{9, 1}}, }) assertQueryPositions(t, string(buff), "$..[?(bool)]", []interface{}{ - queryTestNode{ - true, toml.Position{15, 1}, - }, + queryTestNode{true, toml.Position{15, 1}}, }) } diff -Nru golang-github-pelletier-go-toml-1.6.0/query/query_test.go golang-github-pelletier-go-toml-1.8.1/query/query_test.go --- golang-github-pelletier-go-toml-1.6.0/query/query_test.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/query/query_test.go 2020-09-12 18:42:04.000000000 +0000 @@ -7,25 +7,26 @@ "github.com/pelletier/go-toml" ) -func assertArrayContainsInAnyOrder(t *testing.T, array []interface{}, objects ...interface{}) { +func assertArrayContainsInOrder(t *testing.T, array []interface{}, objects ...interface{}) { if len(array) != len(objects) { t.Fatalf("array contains %d objects but %d are expected", len(array), len(objects)) } - for _, o := range objects { - found := false - for _, a := range array { - if a == o { - found = true - break - } - } - if !found { - t.Fatal(o, "not found in array", array) + for i := 0; i < len(array); i++ { + if array[i] != objects[i] { + t.Fatalf("wanted '%s', have '%s'", objects[i], array[i]) } } } +func checkQuery(t *testing.T, tree *toml.Tree, query string, objects ...interface{}) { + results, err := CompileAndExecute(query, tree) + if err != nil { + t.Fatal("unexpected error:", err) + } + assertArrayContainsInOrder(t, results.Values(), objects...) +} + func TestQueryExample(t *testing.T) { config, _ := toml.Load(` [[book]] @@ -37,16 +38,18 @@ [[book]] title = "Neuromancer" author = "William Gibson" - `) - authors, err := CompileAndExecute("$.book.author", config) - if err != nil { - t.Fatal("unexpected error:", err) - } - names := authors.Values() - if len(names) != 3 { - t.Fatalf("query should return 3 names but returned %d", len(names)) - } - assertArrayContainsInAnyOrder(t, names, "Stephen King", "Ernest Hemmingway", "William Gibson") + `) + + checkQuery(t, config, "$.book.author", "Stephen King", "Ernest Hemmingway", "William Gibson") + + checkQuery(t, config, "$.book[0].author", "Stephen King") + checkQuery(t, config, "$.book[-1].author", "William Gibson") + checkQuery(t, config, "$.book[1:].author", "Ernest Hemmingway", "William Gibson") + checkQuery(t, config, "$.book[-1:].author", "William Gibson") + checkQuery(t, config, "$.book[::2].author", "Stephen King", "William Gibson") + checkQuery(t, config, "$.book[::-1].author", "William Gibson", "Ernest Hemmingway", "Stephen King") + checkQuery(t, config, "$.book[:].author", "Stephen King", "Ernest Hemmingway", "William Gibson") + checkQuery(t, config, "$.book[::].author", "Stephen King", "Ernest Hemmingway", "William Gibson") } func TestQueryReadmeExample(t *testing.T) { @@ -56,16 +59,7 @@ password = "mypassword" `) - query, err := Compile("$..[user,password]") - if err != nil { - t.Fatal("unexpected error:", err) - } - results := query.Execute(config) - values := results.Values() - if len(values) != 2 { - t.Fatalf("query should return 2 values but returned %d", len(values)) - } - assertArrayContainsInAnyOrder(t, values, "pelletier", "mypassword") + checkQuery(t, config, "$..[user,password]", "pelletier", "mypassword") } func TestQueryPathNotPresent(t *testing.T) { diff -Nru golang-github-pelletier-go-toml-1.6.0/query/README.md golang-github-pelletier-go-toml-1.8.1/query/README.md --- golang-github-pelletier-go-toml-1.6.0/query/README.md 1970-01-01 00:00:00.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/query/README.md 2020-09-12 18:42:04.000000000 +0000 @@ -0,0 +1,201 @@ +# Query package + +## Overview + +Package query performs JSONPath-like queries on a TOML document. + +The query path implementation is based loosely on the JSONPath specification: +http://goessner.net/articles/JsonPath/. + +The idea behind a query path is to allow quick access to any element, or set +of elements within TOML document, with a single expression. + +```go +result, err := query.CompileAndExecute("$.foo.bar.baz", tree) +``` + +This is roughly equivalent to: + +```go +next := tree.Get("foo") +if next != nil { + next = next.Get("bar") + if next != nil { + next = next.Get("baz") + } +} +result := next +``` + +err is nil if any parsing exception occurs. + +If no node in the tree matches the query, result will simply contain an empty list of +items. + +As illustrated above, the query path is much more efficient, especially since +the structure of the TOML file can vary. Rather than making assumptions about +a document's structure, a query allows the programmer to make structured +requests into the document, and get zero or more values as a result. + +## Query syntax + +The syntax of a query begins with a root token, followed by any number +sub-expressions: + +``` +$ + Root of the TOML tree. This must always come first. +.name + Selects child of this node, where 'name' is a TOML key + name. +['name'] + Selects child of this node, where 'name' is a string + containing a TOML key name. +[index] + Selcts child array element at 'index'. +..expr + Recursively selects all children, filtered by an a union, + index, or slice expression. +..* + Recursive selection of all nodes at this point in the + tree. +.* + Selects all children of the current node. +[expr,expr] + Union operator - a logical 'or' grouping of two or more + sub-expressions: index, key name, or filter. +[start:end:step] + Slice operator - selects array elements from start to + end-1, at the given step. All three arguments are + optional. +[?(filter)] + Named filter expression - the function 'filter' is + used to filter children at this node. +``` + +## Query Indexes And Slices + +Index expressions perform no bounds checking, and will contribute no +values to the result set if the provided index or index range is invalid. +Negative indexes represent values from the end of the array, counting backwards. + +```go +// select the last index of the array named 'foo' +query.CompileAndExecute("$.foo[-1]", tree) +``` + +Slice expressions are supported, by using ':' to separate a start/end index pair. + +```go +// select up to the first five elements in the array +query.CompileAndExecute("$.foo[0:5]", tree) +``` + +Slice expressions also allow negative indexes for the start and stop +arguments. + +```go +// select all array elements except the last one. +query.CompileAndExecute("$.foo[0:-1]", tree) +``` + +Slice expressions may have an optional stride/step parameter: + +```go +// select every other element +query.CompileAndExecute("$.foo[0::2]", tree) +``` + +Slice start and end parameters are also optional: + +```go +// these are all equivalent and select all the values in the array +query.CompileAndExecute("$.foo[:]", tree) +query.CompileAndExecute("$.foo[::]", tree) +query.CompileAndExecute("$.foo[::1]", tree) +query.CompileAndExecute("$.foo[0:]", tree) +query.CompileAndExecute("$.foo[0::]", tree) +query.CompileAndExecute("$.foo[0::1]", tree) +``` + +## Query Filters + +Query filters are used within a Union [,] or single Filter [] expression. +A filter only allows nodes that qualify through to the next expression, +and/or into the result set. + +```go +// returns children of foo that are permitted by the 'bar' filter. +query.CompileAndExecute("$.foo[?(bar)]", tree) +``` + +There are several filters provided with the library: + +``` +tree + Allows nodes of type Tree. +int + Allows nodes of type int64. +float + Allows nodes of type float64. +string + Allows nodes of type string. +time + Allows nodes of type time.Time. +bool + Allows nodes of type bool. +``` + +## Query Results + +An executed query returns a Result object. This contains the nodes +in the TOML tree that qualify the query expression. Position information +is also available for each value in the set. + +```go +// display the results of a query +results := query.CompileAndExecute("$.foo.bar.baz", tree) +for idx, value := results.Values() { + fmt.Println("%v: %v", results.Positions()[idx], value) +} +``` + +## Compiled Queries + +Queries may be executed directly on a Tree object, or compiled ahead +of time and executed discretely. The former is more convenient, but has the +penalty of having to recompile the query expression each time. + +```go +// basic query +results := query.CompileAndExecute("$.foo.bar.baz", tree) + +// compiled query +query, err := toml.Compile("$.foo.bar.baz") +results := query.Execute(tree) + +// run the compiled query again on a different tree +moreResults := query.Execute(anotherTree) +``` + +## User Defined Query Filters + +Filter expressions may also be user defined by using the SetFilter() +function on the Query object. The function must return true/false, which +signifies if the passed node is kept or discarded, respectively. + +```go +// create a query that references a user-defined filter +query, _ := query.Compile("$[?(bazOnly)]") + +// define the filter, and assign it to the query +query.SetFilter("bazOnly", func(node interface{}) bool{ + if tree, ok := node.(*Tree); ok { + return tree.Has("baz") + } + return false // reject all other node types +}) + +// run the query +query.Execute(tree) +``` diff -Nru golang-github-pelletier-go-toml-1.6.0/query/tokens.go golang-github-pelletier-go-toml-1.8.1/query/tokens.go --- golang-github-pelletier-go-toml-1.6.0/query/tokens.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/query/tokens.go 2020-09-12 18:42:04.000000000 +0000 @@ -2,9 +2,9 @@ import ( "fmt" - "github.com/pelletier/go-toml" "strconv" - "unicode" + + "github.com/pelletier/go-toml" ) // Define tokens @@ -92,11 +92,11 @@ } func isAlphanumeric(r rune) bool { - return unicode.IsLetter(r) || r == '_' + return 'a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' || r == '_' } func isDigit(r rune) bool { - return unicode.IsNumber(r) + return '0' <= r && r <= '9' } func isHexDigit(r rune) bool { diff -Nru golang-github-pelletier-go-toml-1.6.0/README.md golang-github-pelletier-go-toml-1.8.1/README.md --- golang-github-pelletier-go-toml-1.6.0/README.md 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/README.md 2020-09-12 18:42:04.000000000 +0000 @@ -3,7 +3,7 @@ Go library for the [TOML](https://github.com/mojombo/toml) format. This library supports TOML version -[v0.5.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.5.0.md) +[v1.0.0-rc.1](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v1.0.0-rc.1.md) [![GoDoc](https://godoc.org/github.com/pelletier/go-toml?status.svg)](http://godoc.org/github.com/pelletier/go-toml) [![license](https://img.shields.io/github/license/pelletier/go-toml.svg)](https://github.com/pelletier/go-toml/blob/master/LICENSE) @@ -18,7 +18,7 @@ * Load TOML documents from files and string data * Easily navigate TOML structure using Tree -* Mashaling and unmarshaling to and from data structures +* Marshaling and unmarshaling to and from data structures * Line & column position data for all parsed elements * [Query support similar to JSON-Path](query/) * Syntax errors contain line and column numbers @@ -74,7 +74,7 @@ q, _ := query.Compile("$..[user,password]") results := q.Execute(config) for ii, item := range results.Values() { - fmt.Println("Query result %d: %v", ii, item) + fmt.Printf("Query result %d: %v\n", ii, item) } ``` @@ -87,7 +87,7 @@ Go-toml provides two handy command line tools: -* `tomll`: Reads TOML files and lint them. +* `tomll`: Reads TOML files and lints them. ``` go install github.com/pelletier/go-toml/cmd/tomll @@ -99,9 +99,9 @@ go install github.com/pelletier/go-toml/cmd/tomljson tomljson --help ``` - + * `jsontoml`: Reads a JSON file and outputs a TOML representation. - + ``` go install github.com/pelletier/go-toml/cmd/jsontoml jsontoml --help diff -Nru golang-github-pelletier-go-toml-1.6.0/token.go golang-github-pelletier-go-toml-1.8.1/token.go --- golang-github-pelletier-go-toml-1.6.0/token.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/token.go 2020-09-12 18:42:04.000000000 +0000 @@ -1,9 +1,6 @@ package toml -import ( - "fmt" - "unicode" -) +import "fmt" // Define tokens type tokenType int @@ -112,7 +109,7 @@ } func isAlphanumeric(r rune) bool { - return unicode.IsLetter(r) || r == '_' + return 'a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' || r == '_' } func isKeyChar(r rune) bool { @@ -127,7 +124,7 @@ } func isDigit(r rune) bool { - return unicode.IsNumber(r) + return '0' <= r && r <= '9' } func isHexDigit(r rune) bool { diff -Nru golang-github-pelletier-go-toml-1.6.0/toml.go golang-github-pelletier-go-toml-1.8.1/toml.go --- golang-github-pelletier-go-toml-1.6.0/toml.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/toml.go 2020-09-12 18:42:04.000000000 +0000 @@ -23,6 +23,7 @@ values map[string]interface{} // string -> *tomlValue, *Tree, []*Tree comment string commented bool + inline bool position Position } @@ -121,6 +122,89 @@ } } +// GetArray returns the value at key in the Tree. +// It returns []string, []int64, etc type if key has homogeneous lists +// Key is a dot-separated path (e.g. a.b.c) without single/double quoted strings. +// Returns nil if the path does not exist in the tree. +// If keys is of length zero, the current tree is returned. +func (t *Tree) GetArray(key string) interface{} { + if key == "" { + return t + } + return t.GetArrayPath(strings.Split(key, ".")) +} + +// GetArrayPath returns the element in the tree indicated by 'keys'. +// If keys is of length zero, the current tree is returned. +func (t *Tree) GetArrayPath(keys []string) interface{} { + if len(keys) == 0 { + return t + } + subtree := t + for _, intermediateKey := range keys[:len(keys)-1] { + value, exists := subtree.values[intermediateKey] + if !exists { + return nil + } + switch node := value.(type) { + case *Tree: + subtree = node + case []*Tree: + // go to most recent element + if len(node) == 0 { + return nil + } + subtree = node[len(node)-1] + default: + return nil // cannot navigate through other node types + } + } + // branch based on final node type + switch node := subtree.values[keys[len(keys)-1]].(type) { + case *tomlValue: + switch n := node.value.(type) { + case []interface{}: + return getArray(n) + default: + return node.value + } + default: + return node + } +} + +// if homogeneous array, then return slice type object over []interface{} +func getArray(n []interface{}) interface{} { + var s []string + var i64 []int64 + var f64 []float64 + var bl []bool + for _, value := range n { + switch v := value.(type) { + case string: + s = append(s, v) + case int64: + i64 = append(i64, v) + case float64: + f64 = append(f64, v) + case bool: + bl = append(bl, v) + default: + return n + } + } + if len(s) == len(n) { + return s + } else if len(i64) == len(n) { + return i64 + } else if len(f64) == len(n) { + return f64 + } else if len(bl) == len(n) { + return bl + } + return n +} + // GetPosition returns the position of the given key. func (t *Tree) GetPosition(key string) Position { if key == "" { @@ -129,6 +213,50 @@ return t.GetPositionPath(strings.Split(key, ".")) } +// SetPositionPath sets the position of element in the tree indicated by 'keys'. +// If keys is of length zero, the current tree position is set. +func (t *Tree) SetPositionPath(keys []string, pos Position) { + if len(keys) == 0 { + t.position = pos + return + } + subtree := t + for _, intermediateKey := range keys[:len(keys)-1] { + value, exists := subtree.values[intermediateKey] + if !exists { + return + } + switch node := value.(type) { + case *Tree: + subtree = node + case []*Tree: + // go to most recent element + if len(node) == 0 { + return + } + subtree = node[len(node)-1] + default: + return + } + } + // branch based on final node type + switch node := subtree.values[keys[len(keys)-1]].(type) { + case *tomlValue: + node.position = pos + return + case *Tree: + node.position = pos + return + case []*Tree: + // go to most recent element + if len(node) == 0 { + return + } + node[len(node)-1].position = pos + return + } +} + // GetPositionPath returns the element in the tree indicated by 'keys'. // If keys is of length zero, the current tree is returned. func (t *Tree) GetPositionPath(keys []string) Position { @@ -211,7 +339,8 @@ // go to most recent element if len(node) == 0 { // create element if it does not exist - subtree.values[intermediateKey] = append(node, newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col})) + node = append(node, newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col})) + subtree.values[intermediateKey] = node } subtree = node[len(node)-1] } @@ -222,11 +351,17 @@ switch v := value.(type) { case *Tree: v.comment = opts.Comment + v.commented = opts.Commented toInsert = value case []*Tree: + for i := range v { + v[i].commented = opts.Commented + } toInsert = value case *tomlValue: v.comment = opts.Comment + v.commented = opts.Commented + v.multiline = opts.Multiline toInsert = v default: toInsert = &tomlValue{value: value, @@ -307,6 +442,7 @@ if !exists { tree := newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col}) tree.position = pos + tree.inline = subtree.inline subtree.values[intermediateKey] = tree nextTree = tree } diff -Nru golang-github-pelletier-go-toml-1.6.0/toml_testgen_test.go golang-github-pelletier-go-toml-1.8.1/toml_testgen_test.go --- golang-github-pelletier-go-toml-1.6.0/toml_testgen_test.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/toml_testgen_test.go 2020-09-12 18:42:04.000000000 +0000 @@ -5,21 +5,6 @@ "testing" ) -func TestInvalidArrayMixedTypesArraysAndInts(t *testing.T) { - input := `arrays-and-ints = [1, ["Arrays are not integers."]]` - testgenInvalid(t, input) -} - -func TestInvalidArrayMixedTypesIntsAndFloats(t *testing.T) { - input := `ints-and-floats = [1, 1.1]` - testgenInvalid(t, input) -} - -func TestInvalidArrayMixedTypesStringsAndInts(t *testing.T) { - input := `strings-and-ints = ["hi", 42]` - testgenInvalid(t, input) -} - func TestInvalidDatetimeMalformedNoLeads(t *testing.T) { input := `no-leads = 1987-7-05T17:45:00Z` testgenInvalid(t, input) diff -Nru golang-github-pelletier-go-toml-1.6.0/toml_test.go golang-github-pelletier-go-toml-1.8.1/toml_test.go --- golang-github-pelletier-go-toml-1.6.0/toml_test.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/toml_test.go 2020-09-12 18:42:04.000000000 +0000 @@ -3,6 +3,7 @@ package toml import ( + "reflect" "testing" ) @@ -39,6 +40,41 @@ } } +func TestTomlGetArray(t *testing.T) { + tree, _ := Load(` + [test] + key = ["one", "two"] + key2 = [true, false, false] + key3 = [1.5,2.5] + `) + + if tree.GetArray("") != tree { + t.Errorf("GetArray should return the tree itself when given an empty path") + } + + expect := []string{"one", "two"} + actual := tree.GetArray("test.key").([]string) + if !reflect.DeepEqual(actual, expect) { + t.Errorf("GetArray should return the []string value") + } + + expect2 := []bool{true, false, false} + actual2 := tree.GetArray("test.key2").([]bool) + if !reflect.DeepEqual(actual2, expect2) { + t.Errorf("GetArray should return the []bool value") + } + + expect3 := []float64{1.5, 2.5} + actual3 := tree.GetArray("test.key3").([]float64) + if !reflect.DeepEqual(actual3, expect3) { + t.Errorf("GetArray should return the []float64 value") + } + + if tree.GetArray(`\`) != nil { + t.Errorf("should return nil when the key is malformed") + } +} + func TestTomlGetDefault(t *testing.T) { tree, _ := Load(` [test] @@ -148,6 +184,51 @@ } } +func TestTomlGetArrayPath(t *testing.T) { + for idx, item := range []struct { + Name string + Path []string + Make func() (tree *Tree, expected interface{}) + }{ + { + Name: "empty", + Path: []string{}, + Make: func() (tree *Tree, expected interface{}) { + tree = newTree() + expected = tree + return + }, + }, + { + Name: "int64", + Path: []string{"a"}, + Make: func() (tree *Tree, expected interface{}) { + var err error + tree, err = Load(`a = [1,2,3]`) + if err != nil { + panic(err) + } + expected = []int64{1, 2, 3} + return + }, + }, + } { + t.Run(item.Name, func(t *testing.T) { + tree, expected := item.Make() + result := tree.GetArrayPath(item.Path) + if !reflect.DeepEqual(result, expected) { + t.Errorf("GetArrayPath[%d] %v - expected %#v, got %#v instead.", idx, item.Path, expected, result) + } + }) + } + + tree, _ := Load("[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6") + if tree.GetArrayPath([]string{"whatever"}) != nil { + t.Error("GetArrayPath should return nil when the key does not exist") + } + +} + func TestTomlFromMap(t *testing.T) { simpleMap := map[string]interface{}{"hello": 42} tree, err := TreeFromMap(simpleMap) diff -Nru golang-github-pelletier-go-toml-1.6.0/tomltree_create.go golang-github-pelletier-go-toml-1.8.1/tomltree_create.go --- golang-github-pelletier-go-toml-1.6.0/tomltree_create.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/tomltree_create.go 2020-09-12 18:42:04.000000000 +0000 @@ -57,6 +57,19 @@ return float64(original), nil case fmt.Stringer: return original.String(), nil + case []interface{}: + value := reflect.ValueOf(original) + length := value.Len() + arrayValue := reflect.MakeSlice(value.Type(), 0, length) + for i := 0; i < length; i++ { + val := value.Index(i).Interface() + simpleValue, err := simpleValueCoercion(val) + if err != nil { + return nil, err + } + arrayValue = reflect.Append(arrayValue, reflect.ValueOf(simpleValue)) + } + return arrayValue.Interface(), nil default: return nil, fmt.Errorf("cannot convert type %T to Tree", object) } diff -Nru golang-github-pelletier-go-toml-1.6.0/tomltree_create_test.go golang-github-pelletier-go-toml-1.8.1/tomltree_create_test.go --- golang-github-pelletier-go-toml-1.6.0/tomltree_create_test.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/tomltree_create_test.go 2020-09-12 18:42:04.000000000 +0000 @@ -1,6 +1,7 @@ package toml import ( + "reflect" "strconv" "testing" "time" @@ -105,7 +106,7 @@ } func TestRoundTripArrayOfTables(t *testing.T) { - orig := "\n[[stuff]]\n name = \"foo\"\n things = [\"a\",\"b\"]\n" + orig := "\n[[stuff]]\n name = \"foo\"\n things = [\"a\", \"b\"]\n" tree, err := Load(orig) if err != nil { t.Fatalf("unexpected error: %s", err) @@ -124,3 +125,119 @@ t.Errorf("want:\n%s\ngot:\n%s", want, got) } } + +func TestTomlSliceOfSlice(t *testing.T) { + tree, err := Load(` hosts=[["10.1.0.107:9092","10.1.0.107:9093", "192.168.0.40:9094"] ] `) + m := tree.ToMap() + tree, err = TreeFromMap(m) + if err != nil { + t.Error("should not error", err) + } + type Struct struct { + Hosts [][]string + } + var actual Struct + tree.Unmarshal(&actual) + + expected := Struct{Hosts: [][]string{[]string{"10.1.0.107:9092", "10.1.0.107:9093", "192.168.0.40:9094"}}} + + if !reflect.DeepEqual(actual, expected) { + t.Errorf("Bad unmarshal: expected %+v, got %+v", expected, actual) + } +} + +func TestTomlSliceOfSliceOfSlice(t *testing.T) { + tree, err := Load(` hosts=[[["10.1.0.107:9092","10.1.0.107:9093", "192.168.0.40:9094"] ]] `) + m := tree.ToMap() + tree, err = TreeFromMap(m) + if err != nil { + t.Error("should not error", err) + } + type Struct struct { + Hosts [][][]string + } + var actual Struct + tree.Unmarshal(&actual) + + expected := Struct{Hosts: [][][]string{[][]string{[]string{"10.1.0.107:9092", "10.1.0.107:9093", "192.168.0.40:9094"}}}} + + if !reflect.DeepEqual(actual, expected) { + t.Errorf("Bad unmarshal: expected %+v, got %+v", expected, actual) + } +} + +func TestTomlSliceOfSliceInt(t *testing.T) { + tree, err := Load(` hosts=[[1,2,3],[4,5,6] ] `) + m := tree.ToMap() + tree, err = TreeFromMap(m) + if err != nil { + t.Error("should not error", err) + } + type Struct struct { + Hosts [][]int + } + var actual Struct + err = tree.Unmarshal(&actual) + if err != nil { + t.Error("should not error", err) + } + + expected := Struct{Hosts: [][]int{[]int{1, 2, 3}, []int{4, 5, 6}}} + + if !reflect.DeepEqual(actual, expected) { + t.Errorf("Bad unmarshal: expected %+v, got %+v", expected, actual) + } + +} +func TestTomlSliceOfSliceInt64(t *testing.T) { + tree, err := Load(` hosts=[[1,2,3],[4,5,6] ] `) + m := tree.ToMap() + tree, err = TreeFromMap(m) + if err != nil { + t.Error("should not error", err) + } + type Struct struct { + Hosts [][]int64 + } + var actual Struct + err = tree.Unmarshal(&actual) + if err != nil { + t.Error("should not error", err) + } + + expected := Struct{Hosts: [][]int64{[]int64{1, 2, 3}, []int64{4, 5, 6}}} + + if !reflect.DeepEqual(actual, expected) { + t.Errorf("Bad unmarshal: expected %+v, got %+v", expected, actual) + } + +} + +func TestTomlSliceOfSliceInt64FromMap(t *testing.T) { + tree, err := TreeFromMap(map[string]interface{}{"hosts": [][]interface{}{[]interface{}{int32(1), int8(2), 3}}}) + if err != nil { + t.Error("should not error", err) + } + type Struct struct { + Hosts [][]int64 + } + var actual Struct + err = tree.Unmarshal(&actual) + if err != nil { + t.Error("should not error", err) + } + + expected := Struct{Hosts: [][]int64{[]int64{1, 2, 3}}} + + if !reflect.DeepEqual(actual, expected) { + t.Errorf("Bad unmarshal: expected %+v, got %+v", expected, actual) + } + +} +func TestTomlSliceOfSliceError(t *testing.T) { // make Codecov happy + _, err := TreeFromMap(map[string]interface{}{"hosts": [][]interface{}{[]interface{}{1, 2, []struct{}{}}}}) + expected := "cannot convert type []struct {} to Tree" + if err.Error() != expected { + t.Fatalf("unexpected error: %s", err) + } +} diff -Nru golang-github-pelletier-go-toml-1.6.0/tomltree_write.go golang-github-pelletier-go-toml-1.8.1/tomltree_write.go --- golang-github-pelletier-go-toml-1.6.0/tomltree_write.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/tomltree_write.go 2020-09-12 18:42:04.000000000 +0000 @@ -28,23 +28,35 @@ // Encodes a string to a TOML-compliant multi-line string value // This function is a clone of the existing encodeTomlString function, except that whitespace characters // are preserved. Quotation marks and backslashes are also not escaped. -func encodeMultilineTomlString(value string) string { +func encodeMultilineTomlString(value string, commented string) string { var b bytes.Buffer + adjacentQuoteCount := 0 - for _, rr := range value { + b.WriteString(commented) + for i, rr := range value { + if rr != '"' { + adjacentQuoteCount = 0 + } else { + adjacentQuoteCount++ + } switch rr { case '\b': b.WriteString(`\b`) case '\t': b.WriteString("\t") case '\n': - b.WriteString("\n") + b.WriteString("\n" + commented) case '\f': b.WriteString(`\f`) case '\r': b.WriteString("\r") case '"': - b.WriteString(`"`) + if adjacentQuoteCount >= 3 || i == len(value)-1 { + adjacentQuoteCount = 0 + b.WriteString(`\"`) + } else { + b.WriteString(`"`) + } case '\\': b.WriteString(`\`) default: @@ -91,7 +103,30 @@ return b.String() } -func tomlValueStringRepresentation(v interface{}, indent string, arraysOneElementPerLine bool) (string, error) { +func tomlTreeStringRepresentation(t *Tree, ord marshalOrder) (string, error) { + var orderedVals []sortNode + switch ord { + case OrderPreserve: + orderedVals = sortByLines(t) + default: + orderedVals = sortAlphabetical(t) + } + + var values []string + for _, node := range orderedVals { + k := node.key + v := t.values[k] + + repr, err := tomlValueStringRepresentation(v, "", "", ord, false) + if err != nil { + return "", err + } + values = append(values, quoteKeyIfNeeded(k)+" = "+repr) + } + return "{ " + strings.Join(values, ", ") + " }", nil +} + +func tomlValueStringRepresentation(v interface{}, commented string, indent string, ord marshalOrder, arraysOneElementPerLine bool) (string, error) { // this interface check is added to dereference the change made in the writeTo function. // That change was made to allow this function to see formatting options. tv, ok := v.(*tomlValue) @@ -123,12 +158,12 @@ return strings.ToLower(strconv.FormatFloat(value, 'f', -1, bits)), nil case string: if tv.multiline { - return "\"\"\"\n" + encodeMultilineTomlString(value) + "\"\"\"", nil + return "\"\"\"\n" + encodeMultilineTomlString(value, commented) + "\"\"\"", nil } return "\"" + encodeTomlString(value) + "\"", nil case []byte: b, _ := v.([]byte) - return tomlValueStringRepresentation(string(b), indent, arraysOneElementPerLine) + return string(b), nil case bool: if value { return "true", nil @@ -142,6 +177,8 @@ return value.String(), nil case LocalTime: return value.String(), nil + case *Tree: + return tomlTreeStringRepresentation(value, ord) case nil: return "", nil } @@ -152,7 +189,7 @@ var values []string for i := 0; i < rv.Len(); i++ { item := rv.Index(i).Interface() - itemRepr, err := tomlValueStringRepresentation(item, indent, arraysOneElementPerLine) + itemRepr, err := tomlValueStringRepresentation(item, commented, indent, ord, arraysOneElementPerLine) if err != nil { return "", err } @@ -166,16 +203,16 @@ for _, value := range values { stringBuffer.WriteString(valueIndent) - stringBuffer.WriteString(value) + stringBuffer.WriteString(commented + value) stringBuffer.WriteString(`,`) stringBuffer.WriteString("\n") } - stringBuffer.WriteString(indent + "]") + stringBuffer.WriteString(indent + commented + "]") return stringBuffer.String(), nil } - return "[" + strings.Join(values, ",") + "]", nil + return "[" + strings.Join(values, ", ") + "]", nil } return "", fmt.Errorf("unsupported value type %T: %v", v, v) } @@ -270,10 +307,10 @@ } func (t *Tree) writeTo(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool) (int64, error) { - return t.writeToOrdered(w, indent, keyspace, bytesCount, arraysOneElementPerLine, OrderAlphabetical) + return t.writeToOrdered(w, indent, keyspace, bytesCount, arraysOneElementPerLine, OrderAlphabetical, " ", false) } -func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool, ord marshalOrder) (int64, error) { +func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool, ord marshalOrder, indentString string, parentCommented bool) (int64, error) { var orderedVals []sortNode switch ord { @@ -289,14 +326,10 @@ k := node.key v := t.values[k] - combinedKey := k + combinedKey := quoteKeyIfNeeded(k) if keyspace != "" { combinedKey = keyspace + "." + combinedKey } - var commented string - if t.commented { - commented = "# " - } switch node := v.(type) { // node has to be of those two types given how keys are sorted above @@ -317,24 +350,33 @@ return bytesCount, errc } } + + var commented string + if parentCommented || t.commented || tv.commented { + commented = "# " + } writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[", combinedKey, "]\n") bytesCount += int64(writtenBytesCount) if err != nil { return bytesCount, err } - bytesCount, err = node.writeToOrdered(w, indent+" ", combinedKey, bytesCount, arraysOneElementPerLine, ord) + bytesCount, err = node.writeToOrdered(w, indent+indentString, combinedKey, bytesCount, arraysOneElementPerLine, ord, indentString, parentCommented || t.commented || tv.commented) if err != nil { return bytesCount, err } case []*Tree: for _, subTree := range node { + var commented string + if parentCommented || t.commented || subTree.commented { + commented = "# " + } writtenBytesCount, err := writeStrings(w, "\n", indent, commented, "[[", combinedKey, "]]\n") bytesCount += int64(writtenBytesCount) if err != nil { return bytesCount, err } - bytesCount, err = subTree.writeToOrdered(w, indent+" ", combinedKey, bytesCount, arraysOneElementPerLine, ord) + bytesCount, err = subTree.writeToOrdered(w, indent+indentString, combinedKey, bytesCount, arraysOneElementPerLine, ord, indentString, parentCommented || t.commented || subTree.commented) if err != nil { return bytesCount, err } @@ -347,7 +389,11 @@ return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k]) } - repr, err := tomlValueStringRepresentation(v, indent, arraysOneElementPerLine) + var commented string + if parentCommented || t.commented || v.commented { + commented = "# " + } + repr, err := tomlValueStringRepresentation(v, commented, indent, ord, arraysOneElementPerLine) if err != nil { return bytesCount, err } @@ -365,10 +411,6 @@ } } - var commented string - if v.commented { - commented = "# " - } quotedKey := quoteKeyIfNeeded(k) writtenBytesCount, err := writeStrings(w, indent, commented, quotedKey, " = ", repr, "\n") bytesCount += int64(writtenBytesCount) diff -Nru golang-github-pelletier-go-toml-1.6.0/tomltree_write_test.go golang-github-pelletier-go-toml-1.8.1/tomltree_write_test.go --- golang-github-pelletier-go-toml-1.6.0/tomltree_write_test.go 2019-10-25 18:53:56.000000000 +0000 +++ golang-github-pelletier-go-toml-1.8.1/tomltree_write_test.go 2020-09-12 18:42:04.000000000 +0000 @@ -236,6 +236,7 @@ []interface{}{"gamma", "delta"}, []interface{}{int64(1), int64(2)}, }, + "score": 4e-08, }, } testMaps(t, tree.ToMap(), expected)