Compare commits

..

22 Commits

Author SHA1 Message Date
Thomas Pelletier 5dc006fb52 Update readme to point to v2 2022-04-27 21:51:02 -04:00
Cameron Moore fed1464066 Fix invalid type assertion in LoadBytes (#733)
Fixes #732
2022-01-05 09:17:32 -05:00
Thomas Pelletier 1baee4630f README: don't mention document editing in v2 2021-12-31 13:31:17 -05:00
Thomas Pelletier 352072d51a Create SECURITY.md 2021-10-17 20:56:41 -04:00
Wendell Sun c42c3365f3 Support unmarshaling up to uint64 (#608) 2021-09-27 19:11:21 -04:00
Bruno FERNANDO b8ba995eaa tomll: add multiLineArray flag to linter (#578) 2021-09-05 21:38:35 -04:00
Thomas Pelletier 8e44986c28 Configure issues templates
Ask people to go to /discussions for anything that is not a bug report.
2021-06-14 10:56:19 -04:00
Thomas Pelletier 837c1d09ee Clarify license and comply with Apache 2.0 (#554) 2021-06-08 13:57:30 -04:00
Thomas Pelletier 8410c965c2 Suggest using go-toml v2 2021-06-03 22:04:06 -04:00
Mikhail f. Shiryaev d083470585 Add Encoder.CompactComments to omit extra new line (#541) 2021-05-12 09:22:40 -04:00
Mikhail f. Shiryaev c893dbf25c Fix empty trees line counting (#539)
Refs #450
2021-05-11 08:50:05 -04:00
dependabot-preview[bot] 2a1df71375 Upgrade to GitHub-native Dependabot (#531)
Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
2021-04-29 19:33:19 -04:00
Ikko Ashimine a2f5197638 Fix typo in README.md (#513)
availble -> available
2021-04-16 08:03:59 -04:00
Thomas Pelletier bb65137dc4 Move v2 warning into a comment 2021-04-10 19:54:52 -04:00
Thomas Pelletier 99782c87cf Add v2 warning in bug report template 2021-04-10 19:50:55 -04:00
Thomas Pelletier ce6fbd7bc0 Support literal multiline marshal (#485)
Use struct tag `multiline:"true" literal:"true"` to enable it.
2021-03-25 20:57:38 -04:00
Sapphire Becker b59c12a70d Remove go-spew dependency (#483) 2021-03-10 20:18:32 -05:00
Thomas Pelletier 6a307ac0d0 Update CI for Go 1.16 (#482)
Fixes #479
2021-03-04 09:58:18 -05:00
Thomas Pelletier a2e5256180 CI should also run on master
Otherwise codecov diff is incorrect.
2021-02-06 08:03:04 -05:00
Thomas Pelletier 5163266f16 Create codeql-analysis.yml (#473) 2021-02-05 08:33:55 -05:00
Vincent Serpoul b4f0a950bf Value string representation public function (#469)
Fixes #468
2021-02-03 08:48:53 -05:00
Thomas Pelletier ef48fb2be1 Expose MarshalOrder (#470)
It is needed as argument to some already public function.

Fixes #459
Ref #469
2021-02-02 12:12:10 -05:00
96 changed files with 16998 additions and 8978 deletions
+11 -2
View File
@@ -1,9 +1,18 @@
---
name: Bug report
about: Create a report to help us improve
---
<!--
‼️ Main development focus is on the upcoming go-toml v2 ⚠️
As a result, v1.x bugs will likely not see a fix on a v1.x version.
However, reporting the bug is the best way to ensure that it will be fixed in v2.
See https://github.com/pelletier/go-toml/discussions/506.
-->
**Describe the bug**
A clear and concise description of what the bug is.
@@ -14,7 +23,7 @@ Steps to reproduce the behavior. Including TOML files.
A clear and concise description of what you expected to happen, if other than "should work".
**Versions**
- go-toml: version (git sha)
- go-toml: version (or git sha)
- go: version
- operating system: e.g. macOS, Windows, Linux
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Questions and discussions
url: https://github.com/pelletier/go-toml/discussions
about: Please ask and answer questions here.
+6 -4
View File
@@ -1,6 +1,8 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/" # Location of package manifests
schedule:
interval: "daily"
- package-ecosystem: gomod
directory: "/"
schedule:
interval: daily
time: "13:00"
open-pull-requests-limit: 10
+1 -1
View File
@@ -13,7 +13,7 @@ name: "CodeQL"
on:
push:
branches: [ master, v2 ]
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
-28
View File
@@ -1,28 +0,0 @@
name: test
on:
push:
branches:
- v2
pull_request:
branches:
- v2
jobs:
build:
strategy:
matrix:
os: [ 'ubuntu-latest', 'windows-latest', 'macos-latest']
go: [ '1.15', '1.16' ]
runs-on: ${{ matrix.os }}
name: ${{ matrix.go }}/${{ matrix.os }}
steps:
- uses: actions/checkout@master
- name: Setup go ${{ matrix.go }}
uses: actions/setup-go@master
with:
go-version: ${{ matrix.go }}
- name: Run unit tests
run: go test -race ./...
- name: Run benchmark tests
run: go test -race ./...
working-directory: benchmark
-81
View File
@@ -1,81 +0,0 @@
[service]
golangci-lint-version = "1.39.0"
[linters-settings.wsl]
allow-assign-and-anything = true
[linters]
disable-all = true
enable = [
"asciicheck",
"bodyclose",
"cyclop",
"deadcode",
"depguard",
"dogsled",
"dupl",
"durationcheck",
"errcheck",
"errorlint",
"exhaustive",
"exhaustivestruct",
"exportloopref",
"forbidigo",
"forcetypeassert",
"funlen",
"gci",
"gochecknoglobals",
"gochecknoinits",
"gocognit",
"goconst",
"gocritic",
"gocyclo",
"godot",
"godox",
"goerr113",
"gofmt",
"gofumpt",
"goheader",
"goimports",
"golint",
"gomnd",
# "gomoddirectives",
"gomodguard",
"goprintffuncname",
"gosec",
"gosimple",
"govet",
"ifshort",
"importas",
"ineffassign",
"lll",
"makezero",
"misspell",
"nakedret",
"nestif",
"nilerr",
"nlreturn",
"noctx",
"nolintlint",
"paralleltest",
"prealloc",
"predeclared",
"revive",
"rowserrcheck",
"sqlclosecheck",
"staticcheck",
"structcheck",
"stylecheck",
# "testpackage",
"thelper",
"tparallel",
"typecheck",
"unconvert",
"unparam",
"unused",
"varcheck",
"wastedassign",
"whitespace",
"wrapcheck",
"wsl"
]
+11
View File
@@ -0,0 +1,11 @@
FROM golang:1.12-alpine3.9 as builder
WORKDIR /go/src/github.com/pelletier/go-toml
COPY . .
ENV CGO_ENABLED=0
ENV GOOS=linux
RUN go install ./...
FROM scratch
COPY --from=builder /go/bin/tomll /usr/bin/tomll
COPY --from=builder /go/bin/tomljson /usr/bin/tomljson
COPY --from=builder /go/bin/jsontoml /usr/bin/jsontoml
+226
View File
@@ -1,3 +1,13 @@
The bulk of github.com/pelletier/go-toml is distributed under the MIT license
(see below), with the exception of localtime.go and localtime.test.go.
Those two files have been copied over from Google's civil library at revision
ed46f5086358513cf8c25f8e3f022cb838a49d66, and are distributed under the Apache
2.0 license (see below).
github.com/pelletier/go-toml:
The MIT License (MIT)
Copyright (c) 2013 - 2021 Thomas Pelletier, Eric Anderton
@@ -19,3 +29,219 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
localtime.go, localtime_test.go:
Originals:
https://raw.githubusercontent.com/googleapis/google-cloud-go/ed46f5086358513cf8c25f8e3f022cb838a49d66/civil/civil.go
https://raw.githubusercontent.com/googleapis/google-cloud-go/ed46f5086358513cf8c25f8e3f022cb838a49d66/civil/civil_test.go
Changes:
* Renamed files from civil* to localtime*.
* Package changed from civil to toml.
* 'Local' prefix added to all structs.
License:
https://raw.githubusercontent.com/googleapis/google-cloud-go/ed46f5086358513cf8c25f8e3f022cb838a49d66/LICENSE
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+29
View File
@@ -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)
+172 -41
View File
@@ -1,58 +1,189 @@
# go-toml V2
# go-toml
Development branch. Use at your own risk.
Go library for the [TOML](https://toml.io/) format.
[👉 Discussion on github](https://github.com/pelletier/go-toml/discussions/471).
* `toml.Unmarshal()` should work as well as v1.
⚠️ This readme is for go-toml v1. As for 2022-04-27,
[go-toml v2](https://github.com/pelletier/go-toml/tree/v2) has been released.
## Must do
The new version contains tons of bug fixes, is much faster, and more
importantly maintained. You are strongly encouraged to use it instead of v1!
### Unmarshal
[👉 go-toml v2](https://github.com/pelletier/go-toml/tree/v2)
- [x] Unmarshal into maps.
- [x] Support Array Tables.
- [x] Unmarshal into pointers.
- [x] Support Date / times.
- [x] Support struct tags annotations.
- [x] Support Arrays.
- [x] Support Unmarshaler interface.
- [x] Original go-toml unmarshal tests pass.
- [x] Benchmark!
- [x] Abstract AST.
- [x] Original go-toml testgen tests pass.
- [x] Track file position (line, column) for errors.
- [ ] Strict mode.
- [ ] Document Unmarshal / Decode
v1 will not receive any updates.
### Marshal
---
- [x] Minimal implementation
- [x] Multiline strings
- [ ] Multiline arrays
- [ ] `inline` tag for tables
- [ ] Optional indentation
- [ ] Option to pick default quotes
This library supports TOML version
[v1.0.0-rc.3](https://toml.io/en/v1.0.0-rc.3)
### Document
[![Go Reference](https://pkg.go.dev/badge/github.com/pelletier/go-toml.svg)](https://pkg.go.dev/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)
[![Build Status](https://dev.azure.com/pelletierthomas/go-toml-ci/_apis/build/status/pelletier.go-toml?branchName=master)](https://dev.azure.com/pelletierthomas/go-toml-ci/_build/latest?definitionId=1&branchName=master)
[![codecov](https://codecov.io/gh/pelletier/go-toml/branch/master/graph/badge.svg)](https://codecov.io/gh/pelletier/go-toml)
[![Go Report Card](https://goreportcard.com/badge/github.com/pelletier/go-toml)](https://goreportcard.com/report/github.com/pelletier/go-toml)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fpelletier%2Fgo-toml.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fpelletier%2Fgo-toml?ref=badge_shield)
- [ ] Gather requirements and design API.
## Ideas
## Development status
- [ ] Allow types to implement a `ASTUnmarshaler` interface to unmarshal
straight from the AST?
- [x] Rewrite AST to use a single array as storage instead of one allocation per
node.
- [ ] Provide "minimal allocations" option that uses `unsafe` to reuse the input
byte array as storage for strings.
- [x] Cache reflection operations per type.
- [ ] Optimize tracker pass.
**️ Consider go-toml v2!**
## Differences with v1
The next version of go-toml is in [active development][v2-dev], and
[nearing completion][v2-map].
* [unmarshal](https://github.com/pelletier/go-toml/discussions/488)
Though technically in beta, v2 is already more tested, [fixes bugs][v1-bugs],
and [much faster][v2-bench]. If you only need reading and writing TOML documents
(majority of cases), those features are implemented and the API unlikely to
change.
The remaining features will be added shortly. While pull-requests are welcome on
v1, no active development is expected on it. When v2.0.0 is released, v1 will be
deprecated.
👉 [go-toml v2][v2]
[v2]: https://github.com/pelletier/go-toml/tree/v2
[v2-map]: https://github.com/pelletier/go-toml/discussions/506
[v2-dev]: https://github.com/pelletier/go-toml/tree/v2
[v1-bugs]: https://github.com/pelletier/go-toml/issues?q=is%3Aissue+is%3Aopen+label%3Av2-fixed
[v2-bench]: https://github.com/pelletier/go-toml/tree/v2#benchmarks
## Features
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
## Import
```go
import "github.com/pelletier/go-toml"
```
## Usage example
Read a TOML document:
```go
config, _ := toml.Load(`
[postgres]
user = "pelletier"
password = "mypassword"`)
// retrieve data directly
user := config.Get("postgres.user").(string)
// or using an intermediate object
postgresConfig := config.Get("postgres").(*toml.Tree)
password := postgresConfig.Get("password").(string)
```
Or use Unmarshal:
```go
type Postgres struct {
User string
Password string
}
type Config struct {
Postgres Postgres
}
doc := []byte(`
[Postgres]
User = "pelletier"
Password = "mypassword"`)
config := Config{}
toml.Unmarshal(doc, &config)
fmt.Println("user=", config.Postgres.User)
```
Or use a query:
```go
// use a query to gather elements without walking the tree
q, _ := query.Compile("$..[user,password]")
results := q.Execute(config)
for ii, item := range results.Values() {
fmt.Printf("Query result %d: %v\n", ii, item)
}
```
## Documentation
The documentation and additional examples are available at
[pkg.go.dev](https://pkg.go.dev/github.com/pelletier/go-toml).
## Tools
Go-toml provides three handy command line tools:
* `tomll`: Reads TOML files and lints them.
```
go install github.com/pelletier/go-toml/cmd/tomll
tomll --help
```
* `tomljson`: Reads a TOML file and outputs its JSON representation.
```
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
```
### Docker image
Those tools are also available as a Docker image from
[dockerhub](https://hub.docker.com/r/pelletier/go-toml). For example, to
use `tomljson`:
```
docker run -v $PWD:/workdir pelletier/go-toml tomljson /workdir/example.toml
```
Only master (`latest`) and tagged versions are published to dockerhub. You
can build your own image as usual:
```
docker build -t go-toml .
```
## Contribute
Feel free to report bugs and patches using GitHub's pull requests system on
[pelletier/go-toml](https://github.com/pelletier/go-toml). Any feedback would be
much appreciated!
### Run tests
`go test ./...`
### Fuzzing
The script `./fuzz.sh` is available to
run [go-fuzz](https://github.com/dvyukov/go-fuzz) on go-toml.
## Versioning
Go-toml follows [Semantic Versioning](http://semver.org/). The supported version
of [TOML](https://github.com/toml-lang/toml) is indicated at the beginning of
this document. The last two major versions of Go are supported
(see [Go Release Policy](https://golang.org/doc/devel/release.html#policy)).
## License
The MIT License (MIT). Read [LICENSE](LICENSE).
The MIT License (MIT) + Apache 2.0. Read [LICENSE](LICENSE).
+19
View File
@@ -0,0 +1,19 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ---------- | ------------------ |
| Latest 2.x | :white_check_mark: |
| All 1.x | :x: |
| All 0.x | :x: |
## Reporting a Vulnerability
Email a vulnerability report to `security@pelletier.codes`. Make sure to include
as many details as possible to reproduce the vulnerability. This is a
side-project: I will try to get back to you as quickly as possible, time
permitting in my personal life. Providing a working patch helps very much!
+188
View File
@@ -0,0 +1,188 @@
trigger:
- master
stages:
- stage: run_checks
displayName: "Check"
dependsOn: []
jobs:
- job: fmt
displayName: "fmt"
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.16"
inputs:
version: "1.16"
- task: Go@0
displayName: "go fmt ./..."
inputs:
command: 'custom'
customCommand: 'fmt'
arguments: './...'
- job: coverage
displayName: "coverage"
pool:
vmImage: ubuntu-latest
steps:
- task: GoTool@0
displayName: "Install Go 1.16"
inputs:
version: "1.16"
- task: Go@0
displayName: "Generate coverage"
inputs:
command: 'test'
arguments: "-race -coverprofile=coverage.txt -covermode=atomic"
- task: Bash@3
inputs:
targetType: 'inline'
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.16"
inputs:
version: "1.16"
- script: echo "##vso[task.setvariable variable=PATH]${PATH}:/home/vsts/go/bin/"
- task: Bash@3
inputs:
filePath: './benchmark.sh'
arguments: "master $(Build.Repository.Uri)"
- job: go_unit_tests
displayName: "unit tests"
strategy:
matrix:
linux 1.16:
goVersion: '1.16'
imageName: 'ubuntu-latest'
mac 1.16:
goVersion: '1.16'
imageName: 'macOS-latest'
windows 1.16:
goVersion: '1.16'
imageName: 'windows-latest'
linux 1.15:
goVersion: '1.15'
imageName: 'ubuntu-latest'
mac 1.15:
goVersion: '1.15'
imageName: 'macOS-latest'
windows 1.15:
goVersion: '1.15'
imageName: 'windows-latest'
pool:
vmImage: $(imageName)
steps:
- task: GoTool@0
displayName: "Install Go $(goVersion)"
inputs:
version: $(goVersion)
- task: Go@0
displayName: "go test ./..."
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.16
- 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"
dependsOn: run_checks
jobs:
- job: build
displayName: "Build"
pool:
vmImage: ubuntu-latest
steps:
- task: Docker@2
inputs:
command: 'build'
Dockerfile: 'Dockerfile'
buildContext: '.'
addPipelineData: false
- stage: publish_docker_image
displayName: "Publish Docker image"
dependsOn: build_docker_image
condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'master'))
jobs:
- job: publish
displayName: "Publish"
pool:
vmImage: ubuntu-latest
steps:
- task: Docker@2
inputs:
containerRegistry: 'DockerHub'
repository: 'pelletier/go-toml'
command: 'buildAndPush'
Dockerfile: 'Dockerfile'
buildContext: '.'
tags: 'latest'
Executable
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
set -ex
reference_ref=${1:-master}
reference_git=${2:-.}
if ! `hash benchstat 2>/dev/null`; then
echo "Installing benchstat"
go get golang.org/x/perf/cmd/benchstat
fi
tempdir=`mktemp -d /tmp/go-toml-benchmark-XXXXXX`
ref_tempdir="${tempdir}/ref"
ref_benchmark="${ref_tempdir}/benchmark-`echo -n ${reference_ref}|tr -s '/' '-'`.txt"
local_benchmark="`pwd`/benchmark-local.txt"
echo "=== ${reference_ref} (${ref_tempdir})"
git clone ${reference_git} ${ref_tempdir} >/dev/null 2>/dev/null
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"
benchstat -delta-test=none ${ref_benchmark} ${local_benchmark}
-94
View File
@@ -1,94 +0,0 @@
package benchmark_test
import (
"compress/gzip"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
var bench_inputs = []struct {
name string
jsonLen int
}{
// from https://gist.githubusercontent.com/feeeper/2197d6d734729625a037af1df14cf2aa/raw/2f22b120e476d897179be3c1e2483d18067aa7df/config.toml
{"config", 806507},
// converted from https://github.com/miloyip/nativejson-benchmark
{"canada", 2090234},
{"citm_catalog", 479897},
{"twitter", 428778},
{"code", 1940472},
// converted from https://raw.githubusercontent.com/mailru/easyjson/master/benchmark/example.json
{"example", 7779},
}
func TestUnmarshalDatasetCode(t *testing.T) {
for _, tc := range bench_inputs {
buf := fixture(t, tc.name)
t.Run(tc.name, func(t *testing.T) {
for _, r := range runners {
if r.name == "bs" && tc.name == "canada" {
t.Skip("skipping: burntsushi can't handle mixed arrays")
}
t.Run(r.name, func(t *testing.T) {
var v interface{}
check(t, r.unmarshal(buf, &v))
b, err := json.Marshal(v)
check(t, err)
require.Equal(t, len(b), tc.jsonLen)
})
}
})
}
}
func BenchmarkUnmarshalDataset(b *testing.B) {
for _, tc := range bench_inputs {
buf := fixture(b, tc.name)
b.Run(tc.name, func(b *testing.B) {
bench(b, func(r runner, b *testing.B) {
if r.name == "bs" && tc.name == "canada" {
b.Skip("skipping: burntsushi can't handle mixed arrays")
}
b.SetBytes(int64(len(buf)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
var v interface{}
check(b, r.unmarshal(buf, &v))
}
})
})
}
}
// fixture returns the uncompressed contents of path.
func fixture(tb testing.TB, path string) []byte {
f, err := os.Open(filepath.Join("testdata", path+".toml.gz"))
check(tb, err)
defer f.Close()
gz, err := gzip.NewReader(f)
check(tb, err)
buf, err := ioutil.ReadAll(gz)
check(tb, err)
return buf
}
func check(tb testing.TB, err error) {
if err != nil {
tb.Helper()
tb.Fatal(err)
}
}
+164
View File
@@ -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": {}
}
}
}
}
+121
View File
@@ -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: {}
+73 -58
View File
@@ -1,50 +1,17 @@
package benchmark_test
package benchmark
import (
"bytes"
"encoding/json"
"io/ioutil"
"testing"
"time"
tomlbs "github.com/BurntSushi/toml"
tomlv1 "github.com/pelletier/go-toml-v1"
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/require"
burntsushi "github.com/BurntSushi/toml"
"github.com/pelletier/go-toml"
"gopkg.in/yaml.v2"
)
type runner struct {
name string
unmarshal func([]byte, interface{}) error
}
var runners = []runner{
{"v2", toml.Unmarshal},
{"v1", tomlv1.Unmarshal},
{"bs", tomlbs.Unmarshal},
}
func bench(b *testing.B, f func(r runner, b *testing.B)) {
for _, r := range runners {
b.Run(r.name, func(b *testing.B) {
f(r, b)
})
}
}
func BenchmarkUnmarshalSimple(b *testing.B) {
bench(b, func(r runner, b *testing.B) {
d := struct {
A string
}{}
doc := []byte(`A = "hello"`)
for i := 0; i < b.N; i++ {
err := r.unmarshal(doc, &d)
if err != nil {
panic(err)
}
}
})
}
type benchmarkDoc struct {
Table struct {
Key string
@@ -151,29 +118,77 @@ type benchmarkDoc struct {
}
}
func BenchmarkReferenceFile(b *testing.B) {
bench(b, func(r runner, b *testing.B) {
bytes, err := ioutil.ReadFile("benchmark.toml")
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)
}
b.SetBytes(int64(len(bytes)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
d := benchmarkDoc{}
err := r.unmarshal(bytes, &d)
if err != nil {
panic(err)
}
}
})
}
}
func TestReferenceFile(t *testing.T) {
func BenchmarkUnmarshalToml(b *testing.B) {
bytes, err := ioutil.ReadFile("benchmark.toml")
require.NoError(t, err)
d := benchmarkDoc{}
err = toml.Unmarshal(bytes, &d)
require.NoError(t, err)
if err != nil {
b.Fatal(err)
}
b.ReportAllocs()
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)
}
}
}
+6 -9
View File
@@ -1,14 +1,11 @@
module github.com/pelletier/go-toml/v2/benchmark
module github.com/pelletier/go-toml/benchmark
go 1.16
replace github.com/pelletier/go-toml/v2 => ../
replace github.com/pelletier/go-toml-v1 => github.com/pelletier/go-toml v1.8.1
go 1.12
require (
github.com/BurntSushi/toml v0.3.1
github.com/pelletier/go-toml-v1 v0.0.0-00010101000000-000000000000
github.com/pelletier/go-toml/v2 v2.0.0-00010101000000-000000000000
github.com/stretchr/testify v1.7.0
github.com/pelletier/go-toml v0.0.0
gopkg.in/yaml.v2 v2.3.0
)
replace github.com/pelletier/go-toml => ../
+2 -12
View File
@@ -1,16 +1,6 @@
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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+82
View File
@@ -0,0 +1,82 @@
// Jsontoml reads JSON and converts to TOML.
//
// Usage:
// cat file.toml | jsontoml > file.json
// jsontoml file1.toml > file.json
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"github.com/pelletier/go-toml"
)
func main() {
flag.Usage = func() {
fmt.Fprintln(os.Stderr, "jsontoml can be used in two ways:")
fmt.Fprintln(os.Stderr, "Writing to STDIN and reading from STDOUT:")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Reading from a file name:")
fmt.Fprintln(os.Stderr, " tomljson file.toml")
}
flag.Parse()
os.Exit(processMain(flag.Args(), os.Stdin, os.Stdout, os.Stderr))
}
func processMain(files []string, defaultInput io.Reader, output io.Writer, errorOutput io.Writer) int {
// read from stdin and print to stdout
inputReader := defaultInput
if len(files) > 0 {
file, err := os.Open(files[0])
if err != nil {
printError(err, errorOutput)
return -1
}
inputReader = file
defer file.Close()
}
s, err := reader(inputReader)
if err != nil {
printError(err, errorOutput)
return -1
}
io.WriteString(output, s)
return 0
}
func printError(err error, output io.Writer) {
io.WriteString(output, err.Error()+"\n")
}
func reader(r io.Reader) (string, error) {
jsonMap := make(map[string]interface{})
jsonBytes, err := ioutil.ReadAll(r)
if err != nil {
return "", err
}
err = json.Unmarshal(jsonBytes, &jsonMap)
if err != nil {
return "", err
}
tree, err := toml.TreeFromMap(jsonMap)
if err != nil {
return "", err
}
return mapToTOML(tree)
}
func mapToTOML(t *toml.Tree) (string, error) {
tomlBytes, err := t.ToTomlString()
if err != nil {
return "", err
}
return string(tomlBytes[:]), nil
}
+92
View File
@@ -0,0 +1,92 @@
package main
import (
"bytes"
"io/ioutil"
"os"
"runtime"
"strings"
"testing"
)
func expectBufferEquality(t *testing.T, name string, buffer *bytes.Buffer, expected string) {
output := buffer.String()
if output != expected {
t.Errorf("incorrect %s: \n%sexpected %s: \n%s", name, output, name, expected)
t.Log([]rune(output))
t.Log([]rune(expected))
}
}
func expectProcessMainResults(t *testing.T, input string, args []string, exitCode int, expectedOutput string, expectedError string) {
inputReader := strings.NewReader(input)
outputBuffer := new(bytes.Buffer)
errorBuffer := new(bytes.Buffer)
returnCode := processMain(args, inputReader, outputBuffer, errorBuffer)
expectBufferEquality(t, "output", outputBuffer, expectedOutput)
expectBufferEquality(t, "error", errorBuffer, expectedError)
if returnCode != exitCode {
t.Error("incorrect return code:", returnCode, "expected", exitCode)
}
}
func TestProcessMainReadFromStdin(t *testing.T) {
expectedOutput := `
[mytoml]
a = 42.0
`
input := `{
"mytoml": {
"a": 42
}
}
`
expectedError := ``
expectedExitCode := 0
expectProcessMainResults(t, input, []string{}, expectedExitCode, expectedOutput, expectedError)
}
func TestProcessMainReadFromFile(t *testing.T) {
input := `{
"mytoml": {
"a": 42
}
}
`
tmpfile, err := ioutil.TempFile("", "example.json")
if err != nil {
t.Fatal(err)
}
if _, err := tmpfile.Write([]byte(input)); err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name())
expectedOutput := `
[mytoml]
a = 42.0
`
expectedError := ``
expectedExitCode := 0
expectProcessMainResults(t, ``, []string{tmpfile.Name()}, expectedExitCode, expectedOutput, expectedError)
}
func TestProcessMainReadFromMissingFile(t *testing.T) {
var expectedError string
if runtime.GOOS == "windows" {
expectedError = `open /this/file/does/not/exist: The system cannot find the path specified.
`
} else {
expectedError = `open /this/file/does/not/exist: no such file or directory
`
}
expectProcessMainResults(t, ``, []string{"/this/file/does/not/exist"}, -1, ``, expectedError)
}
+71
View File
@@ -0,0 +1,71 @@
// Tomljson reads TOML and converts to JSON.
//
// Usage:
// cat file.toml | tomljson > file.json
// tomljson file1.toml > file.json
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"github.com/pelletier/go-toml"
)
func main() {
flag.Usage = func() {
fmt.Fprintln(os.Stderr, "tomljson can be used in two ways:")
fmt.Fprintln(os.Stderr, "Writing to STDIN and reading from STDOUT:")
fmt.Fprintln(os.Stderr, " cat file.toml | tomljson > file.json")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Reading from a file name:")
fmt.Fprintln(os.Stderr, " tomljson file.toml")
}
flag.Parse()
os.Exit(processMain(flag.Args(), os.Stdin, os.Stdout, os.Stderr))
}
func processMain(files []string, defaultInput io.Reader, output io.Writer, errorOutput io.Writer) int {
// read from stdin and print to stdout
inputReader := defaultInput
if len(files) > 0 {
var err error
inputReader, err = os.Open(files[0])
if err != nil {
printError(err, errorOutput)
return -1
}
}
s, err := reader(inputReader)
if err != nil {
printError(err, errorOutput)
return -1
}
io.WriteString(output, s+"\n")
return 0
}
func printError(err error, output io.Writer) {
io.WriteString(output, err.Error()+"\n")
}
func reader(r io.Reader) (string, error) {
tree, err := toml.LoadReader(r)
if err != nil {
return "", err
}
return mapToJSON(tree)
}
func mapToJSON(tree *toml.Tree) (string, error) {
treeMap := tree.ToMap()
bytes, err := json.MarshalIndent(treeMap, "", " ")
if err != nil {
return "", err
}
return string(bytes[:]), nil
}
+90
View File
@@ -0,0 +1,90 @@
package main
import (
"bytes"
"io/ioutil"
"os"
"runtime"
"strings"
"testing"
)
func expectBufferEquality(t *testing.T, name string, buffer *bytes.Buffer, expected string) {
output := buffer.String()
if output != expected {
t.Errorf("incorrect %s:\n%s\n\nexpected %s:\n%s", name, output, name, expected)
t.Log([]rune(output))
t.Log([]rune(expected))
}
}
func expectProcessMainResults(t *testing.T, input string, args []string, exitCode int, expectedOutput string, expectedError string) {
inputReader := strings.NewReader(input)
outputBuffer := new(bytes.Buffer)
errorBuffer := new(bytes.Buffer)
returnCode := processMain(args, inputReader, outputBuffer, errorBuffer)
expectBufferEquality(t, "output", outputBuffer, expectedOutput)
expectBufferEquality(t, "error", errorBuffer, expectedError)
if returnCode != exitCode {
t.Error("incorrect return code:", returnCode, "expected", exitCode)
}
}
func TestProcessMainReadFromStdin(t *testing.T) {
input := `
[mytoml]
a = 42`
expectedOutput := `{
"mytoml": {
"a": 42
}
}
`
expectedError := ``
expectedExitCode := 0
expectProcessMainResults(t, input, []string{}, expectedExitCode, expectedOutput, expectedError)
}
func TestProcessMainReadFromFile(t *testing.T) {
input := `
[mytoml]
a = 42`
tmpfile, err := ioutil.TempFile("", "example.toml")
if err != nil {
t.Fatal(err)
}
if _, err := tmpfile.Write([]byte(input)); err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name())
expectedOutput := `{
"mytoml": {
"a": 42
}
}
`
expectedError := ``
expectedExitCode := 0
expectProcessMainResults(t, ``, []string{tmpfile.Name()}, expectedExitCode, expectedOutput, expectedError)
}
func TestProcessMainReadFromMissingFile(t *testing.T) {
var expectedError string
if runtime.GOOS == "windows" {
expectedError = `open /this/file/does/not/exist: The system cannot find the path specified.
`
} else {
expectedError = `open /this/file/does/not/exist: no such file or directory
`
}
expectProcessMainResults(t, ``, []string{"/this/file/does/not/exist"}, -1, ``, expectedError)
}
+83
View File
@@ -0,0 +1,83 @@
// Tomll is a linter for TOML
//
// Usage:
// cat file.toml | tomll > file_linted.toml
// tomll file1.toml file2.toml # lint the two files in place
package main
import (
"bytes"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"github.com/pelletier/go-toml"
)
func main() {
multiLineArray := flag.Bool("multiLineArray", false, "sets up the linter to encode arrays with more than one element on multiple lines instead of one.")
flag.Usage = func() {
fmt.Fprintln(os.Stderr, "tomll can be used in two ways:")
fmt.Fprintln(os.Stderr, "Writing to STDIN and reading from STDOUT:")
fmt.Fprintln(os.Stderr, " cat file.toml | tomll > file.toml")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Reading and updating a list of files:")
fmt.Fprintln(os.Stderr, " tomll a.toml b.toml c.toml")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "When given a list of files, tomll will modify all files in place without asking.")
fmt.Fprintln(os.Stderr, "When given a list of files, tomll will modify all files in place without asking.")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Flags:")
fmt.Fprintln(os.Stderr, "-multiLineArray sets up the linter to encode arrays with more than one element on multiple lines instead of one.")
}
flag.Parse()
// read from stdin and print to stdout
if flag.NArg() == 0 {
s, err := lintReader(os.Stdin, *multiLineArray)
if err != nil {
io.WriteString(os.Stderr, err.Error())
os.Exit(-1)
}
io.WriteString(os.Stdout, s)
} else {
// otherwise modify a list of files
for _, filename := range flag.Args() {
s, err := lintFile(filename, *multiLineArray)
if err != nil {
io.WriteString(os.Stderr, err.Error())
os.Exit(-1)
}
ioutil.WriteFile(filename, []byte(s), 0644)
}
}
}
func lintFile(filename string, multiLineArray bool) (string, error) {
tree, err := toml.LoadFile(filename)
if err != nil {
return "", err
}
buf := new(bytes.Buffer)
if err := toml.NewEncoder(buf).ArraysWithOneElementPerLine(multiLineArray).Encode(tree); err != nil {
panic(err)
}
return buf.String(), nil
}
func lintReader(r io.Reader, multiLineArray bool) (string, error) {
tree, err := toml.LoadReader(r)
if err != nil {
return "", err
}
buf := new(bytes.Buffer)
if err := toml.NewEncoder(buf).ArraysWithOneElementPerLine(multiLineArray).Encode(tree); err != nil {
panic(err)
}
return buf.String(), nil
}
+219
View File
@@ -0,0 +1,219 @@
// Tomltestgen is a program that retrieves a given version of
// https://github.com/BurntSushi/toml-test and generates go code for go-toml's unit tests
// based on the test files.
//
// Usage: go run github.com/pelletier/go-toml/cmd/tomltestgen > toml_testgen_test.go
package main
import (
"archive/zip"
"bytes"
"flag"
"fmt"
"go/format"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"text/template"
"time"
)
type invalid struct {
Name string
Input string
}
type valid struct {
Name string
Input string
JsonRef string
}
type testsCollection struct {
Ref string
Timestamp string
Invalid []invalid
Valid []valid
Count int
}
const srcTemplate = "// Generated by tomltestgen for toml-test ref {{.Ref}} on {{.Timestamp}}\n" +
"package toml\n" +
" import (\n" +
" \"testing\"\n" +
")\n" +
"{{range .Invalid}}\n" +
"func TestInvalid{{.Name}}(t *testing.T) {\n" +
" input := {{.Input|gostr}}\n" +
" testgenInvalid(t, input)\n" +
"}\n" +
"{{end}}\n" +
"\n" +
"{{range .Valid}}\n" +
"func TestValid{{.Name}}(t *testing.T) {\n" +
" input := {{.Input|gostr}}\n" +
" jsonRef := {{.JsonRef|gostr}}\n" +
" testgenValid(t, input, jsonRef)\n" +
"}\n" +
"{{end}}\n"
func downloadTmpFile(url string) string {
log.Println("starting to download file from", url)
resp, err := http.Get(url)
if err != nil {
panic(err)
}
defer resp.Body.Close()
tmpfile, err := ioutil.TempFile("", "toml-test-*.zip")
if err != nil {
panic(err)
}
defer tmpfile.Close()
copiedLen, err := io.Copy(tmpfile, resp.Body)
if err != nil {
panic(err)
}
if resp.ContentLength > 0 && copiedLen != resp.ContentLength {
panic(fmt.Errorf("copied %d bytes, request body had %d", copiedLen, resp.ContentLength))
}
return tmpfile.Name()
}
func kebabToCamel(kebab string) string {
camel := ""
nextUpper := true
for _, c := range kebab {
if nextUpper {
camel += strings.ToUpper(string(c))
nextUpper = false
} else if c == '-' {
nextUpper = true
} else {
camel += string(c)
}
}
return camel
}
func readFileFromZip(f *zip.File) string {
reader, err := f.Open()
if err != nil {
panic(err)
}
defer reader.Close()
bytes, err := ioutil.ReadAll(reader)
if err != nil {
panic(err)
}
return string(bytes)
}
func templateGoStr(input string) string {
if len(input) > 0 && input[len(input)-1] == '\n' {
input = input[0 : len(input)-1]
}
if strings.Contains(input, "`") {
lines := strings.Split(input, "\n")
for idx, line := range lines {
lines[idx] = strconv.Quote(line + "\n")
}
return strings.Join(lines, " + \n")
}
return "`" + input + "`"
}
var (
ref = flag.String("r", "master", "git reference")
)
func usage() {
_, _ = fmt.Fprintf(os.Stderr, "usage: tomltestgen [flags]\n")
flag.PrintDefaults()
}
func main() {
flag.Usage = usage
flag.Parse()
url := "https://codeload.github.com/BurntSushi/toml-test/zip/" + *ref
resultFile := downloadTmpFile(url)
defer os.Remove(resultFile)
log.Println("file written to", resultFile)
zipReader, err := zip.OpenReader(resultFile)
if err != nil {
panic(err)
}
defer zipReader.Close()
collection := testsCollection{
Ref: *ref,
Timestamp: time.Now().Format(time.RFC3339),
}
zipFilesMap := map[string]*zip.File{}
for _, f := range zipReader.File {
zipFilesMap[f.Name] = f
}
testFileRegexp := regexp.MustCompile(`([^/]+/tests/(valid|invalid)/(.+))\.(toml)`)
for _, f := range zipReader.File {
groups := testFileRegexp.FindStringSubmatch(f.Name)
if len(groups) > 0 {
name := kebabToCamel(groups[3])
testType := groups[2]
log.Printf("> [%s] %s\n", testType, name)
tomlContent := readFileFromZip(f)
switch testType {
case "invalid":
collection.Invalid = append(collection.Invalid, invalid{
Name: name,
Input: tomlContent,
})
collection.Count++
case "valid":
baseFilePath := groups[1]
jsonFilePath := baseFilePath + ".json"
jsonContent := readFileFromZip(zipFilesMap[jsonFilePath])
collection.Valid = append(collection.Valid, valid{
Name: name,
Input: tomlContent,
JsonRef: jsonContent,
})
collection.Count++
default:
panic(fmt.Sprintf("unknown test type: %s", testType))
}
}
}
log.Printf("Collected %d tests from toml-test\n", collection.Count)
funcMap := template.FuncMap{
"gostr": templateGoStr,
}
t := template.Must(template.New("src").Funcs(funcMap).Parse(srcTemplate))
buf := new(bytes.Buffer)
err = t.Execute(buf, collection)
if err != nil {
panic(err)
}
outputBytes, err := format.Source(buf.Bytes())
if err != nil {
panic(err)
}
fmt.Println(string(outputBytes))
}
-381
View File
@@ -1,381 +0,0 @@
package toml
import (
"errors"
"fmt"
"math"
"strconv"
"strings"
"time"
)
func parseInteger(b []byte) (int64, error) {
if len(b) > 2 && b[0] == '0' {
switch b[1] {
case 'x':
return parseIntHex(b)
case 'b':
return parseIntBin(b)
case 'o':
return parseIntOct(b)
default:
return 0, newDecodeError(b[1:2], "invalid base: '%c'", b[1])
}
}
return parseIntDec(b)
}
func parseLocalDate(b []byte) (LocalDate, error) {
// full-date = date-fullyear "-" date-month "-" date-mday
// date-fullyear = 4DIGIT
// date-month = 2DIGIT ; 01-12
// date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year
var date LocalDate
if len(b) != 10 || b[4] != '-' || b[7] != '-' {
return date, newDecodeError(b, "dates are expected to have the format YYYY-MM-DD")
}
var err error
date.Year, err = parseDecimalDigits(b[0:4])
if err != nil {
return date, err
}
v, err := parseDecimalDigits(b[5:7])
if err != nil {
return date, err
}
date.Month = time.Month(v)
date.Day, err = parseDecimalDigits(b[8:10])
if err != nil {
return date, err
}
return date, nil
}
var errNotDigit = errors.New("not a digit")
func parseDecimalDigits(b []byte) (int, error) {
v := 0
for _, c := range b {
if !isDigit(c) {
return 0, fmt.Errorf("%s: %w", b, errNotDigit)
}
v *= 10
v += int(c - '0')
}
return v, nil
}
var errParseDateTimeMissingInfo = errors.New("date-time missing timezone information")
func parseDateTime(b []byte) (time.Time, error) {
// offset-date-time = full-date time-delim full-time
// full-time = partial-time time-offset
// time-offset = "Z" / time-numoffset
// time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
dt, b, err := parseLocalDateTime(b)
if err != nil {
return time.Time{}, err
}
var zone *time.Location
if len(b) == 0 {
return time.Time{}, errParseDateTimeMissingInfo
}
if b[0] == 'Z' {
b = b[1:]
zone = time.UTC
} else {
const dateTimeByteLen = 6
if len(b) != dateTimeByteLen {
return time.Time{}, newDecodeError(b, "invalid date-time timezone")
}
direction := 1
switch b[0] {
case '+':
case '-':
direction = -1
default:
return time.Time{}, newDecodeError(b[0:1], "invalid timezone offset character")
}
hours := digitsToInt(b[1:3])
minutes := digitsToInt(b[4:6])
seconds := direction * (hours*3600 + minutes*60)
zone = time.FixedZone("", seconds)
}
if len(b) > 0 {
return time.Time{}, newDecodeError(b, "extra bytes at the end of the timezone")
}
t := time.Date(
dt.Date.Year,
dt.Date.Month,
dt.Date.Day,
dt.Time.Hour,
dt.Time.Minute,
dt.Time.Second,
dt.Time.Nanosecond,
zone)
return t, nil
}
var (
errParseLocalDateTimeWrongLength = errors.New(
"local datetimes are expected to have the format YYYY-MM-DDTHH:MM:SS[.NNNNNN]",
)
errParseLocalDateTimeWrongSeparator = errors.New("datetime separator is expected to be T or a space")
)
func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
var dt LocalDateTime
const localDateTimeByteLen = 11
if len(b) < localDateTimeByteLen {
return dt, nil, errParseLocalDateTimeWrongLength
}
date, err := parseLocalDate(b[:10])
if err != nil {
return dt, nil, err
}
dt.Date = date
sep := b[10]
if sep != 'T' && sep != ' ' {
return dt, nil, errParseLocalDateTimeWrongSeparator
}
t, rest, err := parseLocalTime(b[11:])
if err != nil {
return dt, nil, err
}
dt.Time = t
return dt, rest, nil
}
var errParseLocalTimeWrongLength = errors.New("times are expected to have the format HH:MM:SS[.NNNNNN]")
// parseLocalTime is a bit different because it also returns the remaining
// []byte that is didn't need. This is to allow parseDateTime to parse those
// remaining bytes as a timezone.
func parseLocalTime(b []byte) (LocalTime, []byte, error) {
var t LocalTime
const localTimeByteLen = 8
if len(b) < localTimeByteLen {
return t, nil, errParseLocalTimeWrongLength
}
var err error
t.Hour, err = parseDecimalDigits(b[0:2])
if err != nil {
return t, nil, err
}
if b[2] != ':' {
return t, nil, newDecodeError(b[2:3], "expecting colon between hours and minutes")
}
t.Minute, err = parseDecimalDigits(b[3:5])
if err != nil {
return t, nil, err
}
if b[5] != ':' {
return t, nil, newDecodeError(b[5:6], "expecting colon between minutes and seconds")
}
t.Second, err = parseDecimalDigits(b[6:8])
if err != nil {
return t, nil, err
}
if len(b) >= 15 && b[8] == '.' {
t.Nanosecond, err = parseDecimalDigits(b[9:15])
if err != nil {
return t, nil, err
}
return t, b[15:], nil
}
return t, b[8:], nil
}
var (
errParseFloatStartDot = errors.New("float cannot start with a dot")
errParseFloatEndDot = errors.New("float cannot end with a dot")
)
//nolint:cyclop
func parseFloat(b []byte) (float64, error) {
//nolint:godox
// TODO: inefficient
if len(b) == 4 && (b[0] == '+' || b[0] == '-') && b[1] == 'n' && b[2] == 'a' && b[3] == 'n' {
return math.NaN(), nil
}
tok := string(b)
err := numberContainsInvalidUnderscore(tok)
if err != nil {
return 0, err
}
cleanedVal := cleanupNumberToken(tok)
if cleanedVal[0] == '.' {
return 0, errParseFloatStartDot
}
if cleanedVal[len(cleanedVal)-1] == '.' {
return 0, errParseFloatEndDot
}
f, err := strconv.ParseFloat(cleanedVal, 64)
if err != nil {
return 0, fmt.Errorf("coudn't ParseFloat %w", err)
}
return f, nil
}
func parseIntHex(b []byte) (int64, error) {
tok := string(b)
cleanedVal := cleanupNumberToken(tok)
err := hexNumberContainsInvalidUnderscore(cleanedVal)
if err != nil {
return 0, err
}
i, err := strconv.ParseInt(cleanedVal[2:], 16, 64)
if err != nil {
return 0, fmt.Errorf("coudn't ParseIntHex %w", err)
}
return i, nil
}
func parseIntOct(b []byte) (int64, error) {
tok := string(b)
cleanedVal := cleanupNumberToken(tok)
err := numberContainsInvalidUnderscore(cleanedVal)
if err != nil {
return 0, err
}
i, err := strconv.ParseInt(cleanedVal[2:], 8, 64)
if err != nil {
return 0, fmt.Errorf("coudn't ParseIntOct %w", err)
}
return i, nil
}
func parseIntBin(b []byte) (int64, error) {
tok := string(b)
cleanedVal := cleanupNumberToken(tok)
err := numberContainsInvalidUnderscore(cleanedVal)
if err != nil {
return 0, err
}
i, err := strconv.ParseInt(cleanedVal[2:], 2, 64)
if err != nil {
return 0, fmt.Errorf("coudn't ParseIntBin %w", err)
}
return i, nil
}
func parseIntDec(b []byte) (int64, error) {
tok := string(b)
cleanedVal := cleanupNumberToken(tok)
err := numberContainsInvalidUnderscore(cleanedVal)
if err != nil {
return 0, err
}
i, err := strconv.ParseInt(cleanedVal, 10, 64)
if err != nil {
return 0, fmt.Errorf("coudn't parseIntDec %w", err)
}
return i, nil
}
func numberContainsInvalidUnderscore(value string) error {
// For large numbers, you may use underscores between digits to enhance
// readability. Each underscore must be surrounded by at least one digit on
// each side.
hasBefore := false
for idx, r := range value {
if r == '_' {
if !hasBefore || idx+1 >= len(value) {
// can't end with an underscore
return errInvalidUnderscore
}
}
hasBefore = isDigitRune(r)
}
return nil
}
func hexNumberContainsInvalidUnderscore(value string) error {
hasBefore := false
for idx, r := range value {
if r == '_' {
if !hasBefore || idx+1 >= len(value) {
// can't end with an underscore
return errInvalidUnderscoreHex
}
}
hasBefore = isHexDigit(r)
}
return nil
}
func cleanupNumberToken(value string) string {
cleanedVal := strings.ReplaceAll(value, "_", "")
return cleanedVal
}
func isHexDigit(r rune) bool {
return isDigitRune(r) ||
(r >= 'a' && r <= 'f') ||
(r >= 'A' && r <= 'F')
}
func isDigitRune(r rune) bool {
return r >= '0' && r <= '9'
}
var (
errInvalidUnderscore = errors.New("invalid use of _ in number")
errInvalidUnderscoreHex = errors.New("invalid use of _ in hex number")
)
+23
View File
@@ -0,0 +1,23 @@
// Package toml is a TOML parser and manipulation library.
//
// This version supports the specification as described in
// https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.5.0.md
//
// Marshaling
//
// Go-toml can marshal and unmarshal TOML documents from and to data
// structures.
//
// TOML document as a tree
//
// Go-toml can operate on a TOML document as a tree. Use one of the Load*
// functions to parse TOML data and obtain a Tree instance, then one of its
// methods to manipulate the tree.
//
// JSONPath-like queries
//
// The package github.com/pelletier/go-toml/query implements a system
// similar to JSONPath to quickly retrieve elements of a TOML document using a
// single expression. See the package documentation for more information.
//
package toml
+170
View File
@@ -0,0 +1,170 @@
// code examples for godoc
package toml_test
import (
"fmt"
"log"
"os"
toml "github.com/pelletier/go-toml"
)
func Example_tree() {
config, err := toml.LoadFile("config.toml")
if err != nil {
fmt.Println("Error ", err.Error())
} else {
// retrieve data directly
directUser := config.Get("postgres.user").(string)
directPassword := config.Get("postgres.password").(string)
fmt.Println("User is", directUser, " and password is", directPassword)
// or using an intermediate object
configTree := config.Get("postgres").(*toml.Tree)
user := configTree.Get("user").(string)
password := configTree.Get("password").(string)
fmt.Println("User is", user, " and password is", password)
// show where elements are in the file
fmt.Printf("User position: %v\n", configTree.GetPosition("user"))
fmt.Printf("Password position: %v\n", configTree.GetPosition("password"))
}
}
func Example_unmarshal() {
type Employer struct {
Name string
Phone string
}
type Person struct {
Name string
Age int64
Employer Employer
}
document := []byte(`
name = "John"
age = 30
[employer]
name = "Company Inc."
phone = "+1 234 567 89012"
`)
person := Person{}
toml.Unmarshal(document, &person)
fmt.Println(person.Name, "is", person.Age, "and works at", person.Employer.Name)
// Output:
// John is 30 and works at Company Inc.
}
func ExampleMarshal() {
type Postgres struct {
User string `toml:"user"`
Password string `toml:"password"`
Database string `toml:"db" commented:"true" comment:"not used anymore"`
}
type Config struct {
Postgres Postgres `toml:"postgres" comment:"Postgres configuration"`
}
config := Config{Postgres{User: "pelletier", Password: "mypassword", Database: "old_database"}}
b, err := toml.Marshal(config)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(b))
// Output:
// # Postgres configuration
// [postgres]
//
// # not used anymore
// # db = "old_database"
// password = "mypassword"
// user = "pelletier"
}
func ExampleUnmarshal() {
type Postgres struct {
User string
Password string
}
type Config struct {
Postgres Postgres
}
doc := []byte(`
[postgres]
user = "pelletier"
password = "mypassword"`)
config := Config{}
toml.Unmarshal(doc, &config)
fmt.Println("user=", config.Postgres.User)
// 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"
}
-223
View File
@@ -1,223 +0,0 @@
package toml
import (
"fmt"
"strconv"
"strings"
"github.com/pelletier/go-toml/v2/internal/unsafe"
)
// DecodeError represents an error encountered during the parsing or decoding
// of a TOML document.
//
// In addition to the error message, it contains the position in the document
// where it happened, as well as a human-readable representation that shows
// where the error occurred in the document.
type DecodeError struct {
message string
line int
column int
human string
}
// internal version of DecodeError that is used as the base to create a
// DecodeError with full context.
type decodeError struct {
highlight []byte
message string
}
func (de *decodeError) Error() string {
return de.message
}
func newDecodeError(highlight []byte, format string, args ...interface{}) error {
return &decodeError{
highlight: highlight,
message: fmt.Sprintf(format, args...),
}
}
// Error returns the error message contained in the DecodeError.
func (e *DecodeError) Error() string {
return e.message
}
// String returns the human-readable contextualized error. This string is multi-line.
func (e *DecodeError) String() string {
return e.human
}
// Position returns the (line, column) pair indicating where the error
// occurred in the document. Positions are 1-indexed.
func (e *DecodeError) Position() (row int, column int) {
return e.line, e.column
}
// decodeErrorFromHighlight creates a DecodeError referencing to a highlighted
// range of bytes from document.
//
// highlight needs to be a sub-slice of document, or this function panics.
//
// The function copies all bytes used in DecodeError, so that document and
// highlight can be freely deallocated.
//nolint:funlen
func wrapDecodeError(document []byte, de *decodeError) error {
if de == nil {
return nil
}
offset := unsafe.SubsliceOffset(document, de.highlight)
errMessage := de.message
errLine, errColumn := positionAtEnd(document[:offset])
before, after := linesOfContext(document, de.highlight, offset, 3)
var buf strings.Builder
maxLine := errLine + len(after) - 1
lineColumnWidth := len(strconv.Itoa(maxLine))
for i := len(before) - 1; i > 0; i-- {
line := errLine - i
buf.WriteString(formatLineNumber(line, lineColumnWidth))
buf.WriteString("|")
if len(before[i]) > 0 {
buf.WriteString(" ")
buf.Write(before[i])
}
buf.WriteRune('\n')
}
buf.WriteString(formatLineNumber(errLine, lineColumnWidth))
buf.WriteString("| ")
if len(before) > 0 {
buf.Write(before[0])
}
buf.Write(de.highlight)
if len(after) > 0 {
buf.Write(after[0])
}
buf.WriteRune('\n')
buf.WriteString(strings.Repeat(" ", lineColumnWidth))
buf.WriteString("| ")
if len(before) > 0 {
buf.WriteString(strings.Repeat(" ", len(before[0])))
}
buf.WriteString(strings.Repeat("~", len(de.highlight)))
if len(errMessage) > 0 {
buf.WriteString(" ")
buf.WriteString(errMessage)
}
for i := 1; i < len(after); i++ {
buf.WriteRune('\n')
line := errLine + i
buf.WriteString(formatLineNumber(line, lineColumnWidth))
buf.WriteString("|")
if len(after[i]) > 0 {
buf.WriteString(" ")
buf.Write(after[i])
}
}
return &DecodeError{
message: errMessage,
line: errLine,
column: errColumn,
human: buf.String(),
}
}
func formatLineNumber(line int, width int) string {
format := "%" + strconv.Itoa(width) + "d"
return fmt.Sprintf(format, line)
}
func linesOfContext(document []byte, highlight []byte, offset int, linesAround int) ([][]byte, [][]byte) {
return beforeLines(document, offset, linesAround), afterLines(document, highlight, offset, linesAround)
}
func beforeLines(document []byte, offset int, linesAround int) [][]byte {
var beforeLines [][]byte
// Walk the document backward from the highlight to find previous lines
// of context.
rest := document[:offset]
backward:
for o := len(rest) - 1; o >= 0 && len(beforeLines) <= linesAround && len(rest) > 0; {
switch {
case rest[o] == '\n':
// handle individual lines
beforeLines = append(beforeLines, rest[o+1:])
rest = rest[:o]
o = len(rest) - 1
case o == 0:
// add the first line only if it's non-empty
beforeLines = append(beforeLines, rest)
break backward
default:
o--
}
}
return beforeLines
}
func afterLines(document []byte, highlight []byte, offset int, linesAround int) [][]byte {
var afterLines [][]byte
// Walk the document forward from the highlight to find the following
// lines of context.
rest := document[offset+len(highlight):]
forward:
for o := 0; o < len(rest) && len(afterLines) <= linesAround; {
switch {
case rest[o] == '\n':
// handle individual lines
afterLines = append(afterLines, rest[:o])
rest = rest[o+1:]
o = 0
case o == len(rest)-1 && o > 0:
// add last line only if it's non-empty
afterLines = append(afterLines, rest)
break forward
default:
o++
}
}
return afterLines
}
func positionAtEnd(b []byte) (row int, column int) {
row = 1
column = 1
for _, c := range b {
if c == '\n' {
row++
column = 1
} else {
column++
}
}
return
}
-181
View File
@@ -1,181 +0,0 @@
package toml
import (
"bytes"
"errors"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
//nolint:funlen
func TestDecodeError(t *testing.T) {
t.Parallel()
examples := []struct {
desc string
doc [3]string
msg string
expected string
}{
{
desc: "no context",
doc: [3]string{"", "morning", ""},
msg: "this is wrong",
expected: `
1| morning
| ~~~~~~~ this is wrong`,
},
{
desc: "one line",
doc: [3]string{"good ", "morning", " everyone"},
msg: "this is wrong",
expected: `
1| good morning everyone
| ~~~~~~~ this is wrong`,
},
{
desc: "exactly 3 lines",
doc: [3]string{`line1
line2
line3
before `, "highlighted", ` after
post line 1
post line 2
post line 3`},
msg: "this is wrong",
expected: `
1| line1
2| line2
3| line3
4| before highlighted after
| ~~~~~~~~~~~ this is wrong
5| post line 1
6| post line 2
7| post line 3`,
},
{
desc: "more than 3 lines",
doc: [3]string{`should not be seen1
should not be seen2
line1
line2
line3
before `, "highlighted", ` after
post line 1
post line 2
post line 3
should not be seen3
should not be seen4`},
msg: "this is wrong",
expected: `
3| line1
4| line2
5| line3
6| before highlighted after
| ~~~~~~~~~~~ this is wrong
7| post line 1
8| post line 2
9| post line 3`,
},
{
desc: "more than 10 total lines",
doc: [3]string{`should not be seen 0
should not be seen1
should not be seen2
should not be seen3
line1
line2
line3
before `, "highlighted", ` after
post line 1
post line 2
post line 3
should not be seen3
should not be seen4`},
msg: "this is wrong",
expected: `
5| line1
6| line2
7| line3
8| before highlighted after
| ~~~~~~~~~~~ this is wrong
9| post line 1
10| post line 2
11| post line 3`,
},
{
desc: "last line of more than 10",
doc: [3]string{`should not be seen
should not be seen
should not be seen
should not be seen
should not be seen
should not be seen
should not be seen
line1
line2
line3
before `, "highlighted", ``},
msg: "this is wrong",
expected: `
8| line1
9| line2
10| line3
11| before highlighted
| ~~~~~~~~~~~ this is wrong
`,
},
{
desc: "handle empty lines in the before/after blocks",
doc: [3]string{
`line1
line 2
before `, "highlighted", ` after
line 3
line 4
line 5`,
},
expected: `1| line1
2|
3| line 2
4| before highlighted after
| ~~~~~~~~~~~
5| line 3
6|
7| line 4`,
},
}
for _, e := range examples {
e := e
t.Run(e.desc, func(t *testing.T) {
t.Parallel()
b := bytes.Buffer{}
b.Write([]byte(e.doc[0]))
start := b.Len()
b.Write([]byte(e.doc[1]))
end := b.Len()
b.Write([]byte(e.doc[2]))
doc := b.Bytes()
hl := doc[start:end]
err := wrapDecodeError(doc, &decodeError{
highlight: hl,
message: e.msg,
})
var derr *DecodeError
if !errors.As(err, &derr) {
t.Errorf("error not in expected format")
return
}
assert.Equal(t, strings.Trim(e.expected, "\n"), derr.String())
})
}
}
+30
View File
@@ -0,0 +1,30 @@
# This is a TOML document. Boom.
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# You can indent as you please. Tabs or spaces. TOML don't care.
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[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
+30
View File
@@ -0,0 +1,30 @@
# This is a TOML document. Boom.
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# You can indent as you please. Tabs or spaces. TOML don't care.
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[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
+31
View File
@@ -0,0 +1,31 @@
// +build gofuzz
package toml
func Fuzz(data []byte) int {
tree, err := LoadBytes(data)
if err != nil {
if tree != nil {
panic("tree must be nil if there is an error")
}
return 0
}
str, err := tree.ToTomlString()
if err != nil {
if str != "" {
panic(`str must be "" if there is an error`)
}
panic(err)
}
tree, err = Load(str)
if err != nil {
if tree != nil {
panic("tree must be nil if there is an error")
}
return 0
}
return 1
}
Executable
+15
View File
@@ -0,0 +1,15 @@
#! /bin/sh
set -eu
go get github.com/dvyukov/go-fuzz/go-fuzz
go get github.com/dvyukov/go-fuzz/go-fuzz-build
if [ ! -e toml-fuzz.zip ]; then
go-fuzz-build github.com/pelletier/go-toml
fi
rm -fr fuzz
mkdir -p fuzz/corpus
cp *.toml fuzz/corpus
go-fuzz -bin=toml-fuzz.zip -workdir=fuzz
+2 -4
View File
@@ -1,5 +1,3 @@
module github.com/pelletier/go-toml/v2
module github.com/pelletier/go-toml
go 1.15
require github.com/stretchr/testify v1.7.0
go 1.12
-11
View File
@@ -1,11 +0,0 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-138
View File
@@ -1,138 +0,0 @@
package ast
import (
"fmt"
)
// Iterator starts uninitialized, you need to call Next() first.
//
// For example:
//
// it := n.Children()
// for it.Next() {
// it.Node()
// }
type Iterator struct {
started bool
node Node
}
// Next moves the iterator forward and returns true if points to a node, false
// otherwise.
func (c *Iterator) Next() bool {
if !c.started {
c.started = true
} else if c.node.Valid() {
c.node = c.node.Next()
}
return c.node.Valid()
}
// Node returns a copy of the node pointed at by the iterator.
func (c *Iterator) Node() Node {
return c.node
}
// Root contains a full AST.
//
// It is immutable once constructed with Builder.
type Root struct {
nodes []Node
}
// Iterator over the top level nodes.
func (r *Root) Iterator() Iterator {
it := Iterator{}
if len(r.nodes) > 0 {
it.node = r.nodes[0]
}
return it
}
func (r *Root) at(idx int) Node {
// TODO: unsafe to point to the node directly
return r.nodes[idx]
}
// Arrays have one child per element in the array.
// InlineTables have one child per key-value pair in the table.
// KeyValues have at least two children. The first one is the value. The
// rest make a potentially dotted key.
// Table and Array table have one child per element of the key they
// represent (same as KeyValue, but without the last node being the value).
// children []Node
type Node struct {
Kind Kind
Data []byte // Raw bytes from the input
// next idx (in the root array). 0 if last of the collection.
next int
// child idx (in the root array). 0 if no child.
child int
// pointer to the root array
root *Root
}
// Next returns a copy of the next node, or an invalid Node if there is no
// next node.
func (n Node) Next() Node {
if n.next <= 0 {
return noNode
}
return n.root.at(n.next)
}
// Child returns a copy of the first child node of this node. Other children
// can be accessed calling Next on the first child.
// Returns an invalid Node if there is none.
func (n Node) Child() Node {
if n.child <= 0 {
return noNode
}
return n.root.at(n.child)
}
// Valid returns true if the node's kind is set (not to Invalid).
func (n Node) Valid() bool {
return n.Kind != Invalid
}
var noNode = Node{}
// Key returns the child nodes making the Key on a supported node. Panics
// otherwise.
// They are guaranteed to be all be of the Kind Key. A simple key would return
// just one element.
func (n *Node) Key() Iterator {
switch n.Kind {
case KeyValue:
value := n.Child()
if !value.Valid() {
panic(fmt.Errorf("KeyValue should have at least two children"))
}
return Iterator{node: value.Next()}
case Table, ArrayTable:
return Iterator{node: n.Child()}
default:
panic(fmt.Errorf("Key() is not supported on a %s", n.Kind))
}
}
// Value returns a pointer to the value node of a KeyValue.
// Guaranteed to be non-nil.
// Panics if not called on a KeyValue node, or if the Children are malformed.
func (n Node) Value() Node {
assertKind(KeyValue, n)
return n.Child()
}
// Children returns an iterator over a node's children.
func (n Node) Children() Iterator {
return Iterator{node: n.Child()}
}
func assertKind(k Kind, n Node) {
if n.Kind != k {
panic(fmt.Errorf("method was expecting a %s, not a %s", k, n.Kind))
}
}
-60
View File
@@ -1,60 +0,0 @@
package ast
type Reference struct {
idx int
set bool
}
func (r Reference) Valid() bool {
return r.set
}
type Builder struct {
tree Root
lastIdx int
}
func (b *Builder) Tree() *Root {
return &b.tree
}
func (b *Builder) NodeAt(ref Reference) Node {
return b.tree.at(ref.idx)
}
func (b *Builder) Reset() {
b.tree.nodes = b.tree.nodes[:0]
b.lastIdx = 0
}
func (b *Builder) Push(n Node) Reference {
n.root = &b.tree
b.lastIdx = len(b.tree.nodes)
b.tree.nodes = append(b.tree.nodes, n)
return Reference{
idx: b.lastIdx,
set: true,
}
}
func (b *Builder) PushAndChain(n Node) Reference {
n.root = &b.tree
newIdx := len(b.tree.nodes)
b.tree.nodes = append(b.tree.nodes, n)
if b.lastIdx >= 0 {
b.tree.nodes[b.lastIdx].next = newIdx
}
b.lastIdx = newIdx
return Reference{
idx: b.lastIdx,
set: true,
}
}
func (b *Builder) AttachChild(parent Reference, child Reference) {
b.tree.nodes[parent.idx].child = child.idx
}
func (b *Builder) Chain(from Reference, to Reference) {
b.tree.nodes[from.idx].next = to.idx
}
-69
View File
@@ -1,69 +0,0 @@
package ast
import "fmt"
type Kind int
const (
// meta
Invalid Kind = iota
Comment
Key
// top level structures
Table
ArrayTable
KeyValue
// containers values
Array
InlineTable
// values
String
Bool
Float
Integer
LocalDate
LocalDateTime
DateTime
Time
)
func (k Kind) String() string {
switch k {
case Invalid:
return "Invalid"
case Comment:
return "Comment"
case Key:
return "Key"
case Table:
return "Table"
case ArrayTable:
return "ArrayTable"
case KeyValue:
return "KeyValue"
case Array:
return "Array"
case InlineTable:
return "InlineTable"
case String:
return "String"
case Bool:
return "Bool"
case Float:
return "Float"
case Integer:
return "Integer"
case LocalDate:
return "LocalDate"
case LocalDateTime:
return "LocalDateTime"
case DateTime:
return "DateTime"
case Time:
return "Time"
}
panic(fmt.Errorf("Kind.String() not implemented for '%d'", k))
}
@@ -1,166 +0,0 @@
package imported_tests
// Those tests have been imported from v1, but adjust to match the new
// defaults of v2.
import (
"testing"
"time"
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/require"
)
func TestDocMarshal(t *testing.T) {
type testDoc struct {
Title string `toml:"title"`
BasicLists testDocBasicLists `toml:"basic_lists"`
SubDocPtrs []*testSubDoc `toml:"subdocptrs"`
BasicMap map[string]string `toml:"basic_map"`
Subdocs testDocSubs `toml:"subdoc"`
Basics testDocBasics `toml:"basic"`
SubDocList []testSubDoc `toml:"subdoclist"`
err int `toml:"shouldntBeHere"`
unexported int `toml:"shouldntBeHere"`
Unexported2 int `toml:"-"`
}
var docData = testDoc{
Title: "TOML Marshal Testing",
unexported: 0,
Unexported2: 0,
Basics: testDocBasics{
Bool: true,
Date: time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC),
Float32: 123.4,
Float64: 123.456782132399,
Int: 5000,
Uint: 5001,
String: &biteMe,
unexported: 0,
},
BasicLists: testDocBasicLists{
Floats: []*float32{&float1, &float2, &float3},
Bools: []bool{true, false, true},
Dates: []time.Time{
time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC),
time.Date(1980, 5, 27, 7, 32, 0, 0, time.UTC),
},
Ints: []int{8001, 8001, 8002},
Strings: []string{"One", "Two", "Three"},
UInts: []uint{5002, 5003},
},
BasicMap: map[string]string{
"one": "one",
"two": "two",
},
Subdocs: testDocSubs{
First: testSubDoc{"First", 0},
Second: &subdoc,
},
SubDocList: []testSubDoc{
{"List.First", 0},
{"List.Second", 0},
},
SubDocPtrs: []*testSubDoc{&subdoc},
}
marshalTestToml := `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']
[[subdocptrs]]
name = 'Second'
[basic_map]
one = 'one'
two = 'two'
[subdoc]
[subdoc.second]
name = 'Second'
[subdoc.first]
name = 'First'
[basic]
uint = 5001
bool = true
float = 123.4
float64 = 123.456782132399
int = 5000
string = 'Bite me'
date = 1979-05-27T07:32:00Z
[[subdoclist]]
name = 'List.First'
[[subdoclist]]
name = 'List.Second'
`
result, err := toml.Marshal(docData)
require.NoError(t, err)
require.Equal(t, marshalTestToml, string(result))
}
func TestBasicMarshalQuotedKey(t *testing.T) {
result, err := toml.Marshal(quotedKeyMarshalTestData)
require.NoError(t, err)
expected := `'Z.string-àéù' = 'Hello'
'Yfloat-𝟘' = 3.5
['Xsubdoc-àéù']
String2 = 'One'
[['W.sublist-𝟘']]
String2 = 'Two'
[['W.sublist-𝟘']]
String2 = 'Three'
`
require.Equal(t, string(expected), string(result))
}
func TestEmptyMarshal(t *testing.T) {
type emptyMarshalTestStruct struct {
Title string `toml:"title"`
Bool bool `toml:"bool"`
Int int `toml:"int"`
String string `toml:"string"`
StringList []string `toml:"stringlist"`
Ptr *basicMarshalTestStruct `toml:"ptr"`
Map map[string]string `toml:"map"`
}
doc := emptyMarshalTestStruct{
Title: "Placeholder",
Bool: false,
Int: 0,
String: "",
StringList: []string{},
Ptr: nil,
Map: map[string]string{},
}
result, err := toml.Marshal(doc)
require.NoError(t, err)
expected := `title = 'Placeholder'
bool = false
int = 0
string = ''
stringlist = []
[map]
`
require.Equal(t, string(expected), string(result))
}
File diff suppressed because it is too large Load Diff
-200
View File
@@ -1,200 +0,0 @@
package tracker
import (
"fmt"
"github.com/pelletier/go-toml/v2/internal/ast"
)
type keyKind uint8
const (
invalidKind keyKind = iota
valueKind
tableKind
arrayTableKind
)
func (k keyKind) String() string {
switch k {
case invalidKind:
return "invalid"
case valueKind:
return "value"
case tableKind:
return "table"
case arrayTableKind:
return "array table"
}
panic("missing keyKind string mapping")
}
// Tracks which keys have been seen with which TOML type to flag duplicates
// and mismatches according to the spec.
type Seen struct {
root *info
current *info
}
type info struct {
parent *info
kind keyKind
children map[string]*info
explicit bool
}
func (i *info) Clear() {
i.children = nil
}
func (i *info) Has(k string) (*info, bool) {
c, ok := i.children[k]
return c, ok
}
func (i *info) SetKind(kind keyKind) {
i.kind = kind
}
func (i *info) CreateTable(k string, explicit bool) *info {
return i.createChild(k, tableKind, explicit)
}
func (i *info) CreateArrayTable(k string, explicit bool) *info {
return i.createChild(k, arrayTableKind, explicit)
}
func (i *info) createChild(k string, kind keyKind, explicit bool) *info {
if i.children == nil {
i.children = make(map[string]*info, 1)
}
x := &info{
parent: i,
kind: kind,
explicit: explicit,
}
i.children[k] = x
return x
}
// CheckExpression takes a top-level node and checks that it does not contain keys
// that have been seen in previous calls, and validates that types are consistent.
func (s *Seen) CheckExpression(node ast.Node) error {
if s.root == nil {
s.root = &info{
kind: tableKind,
}
s.current = s.root
}
switch node.Kind {
case ast.KeyValue:
return s.checkKeyValue(s.current, node)
case ast.Table:
return s.checkTable(node)
case ast.ArrayTable:
return s.checkArrayTable(node)
default:
panic(fmt.Errorf("this should not be a top level node type: %s", node.Kind))
}
}
func (s *Seen) checkTable(node ast.Node) error {
s.current = s.root
it := node.Key()
// handle the first parts of the key, excluding the last one
for it.Next() {
if !it.Node().Next().Valid() {
break
}
k := string(it.Node().Data)
child, found := s.current.Has(k)
if !found {
child = s.current.CreateTable(k, false)
}
s.current = child
}
// handle the last part of the key
k := string(it.Node().Data)
i, found := s.current.Has(k)
if found {
if i.kind != tableKind {
return fmt.Errorf("key %s should be a table", k)
}
if i.explicit {
return fmt.Errorf("table %s already exists", k)
}
i.explicit = true
s.current = i
} else {
s.current = s.current.CreateTable(k, true)
}
return nil
}
func (s *Seen) checkArrayTable(node ast.Node) error {
s.current = s.root
it := node.Key()
// handle the first parts of the key, excluding the last one
for it.Next() {
if !it.Node().Next().Valid() {
break
}
k := string(it.Node().Data)
child, found := s.current.Has(k)
if !found {
child = s.current.CreateTable(k, false)
}
s.current = child
}
// handle the last part of the key
k := string(it.Node().Data)
info, found := s.current.Has(k)
if found {
if info.kind != arrayTableKind {
return fmt.Errorf("key %s already exists but is not an array table", k)
}
info.Clear()
} else {
info = s.current.CreateArrayTable(k, true)
}
s.current = info
return nil
}
func (s *Seen) checkKeyValue(context *info, node ast.Node) error {
it := node.Key()
// handle the first parts of the key, excluding the last one
for it.Next() {
k := string(it.Node().Data)
child, found := context.Has(k)
if found {
if child.kind != tableKind {
return fmt.Errorf("expected %s to be a table, not a %s", k, child.kind)
}
} else {
child = context.CreateTable(k, false)
}
context = child
}
if node.Value().Kind == ast.InlineTable {
context.SetKind(tableKind)
} else {
context.SetKind(valueKind)
}
return nil
}
-35
View File
@@ -1,35 +0,0 @@
package unsafe
import (
"fmt"
"reflect"
"unsafe"
)
const maxInt = uintptr(int(^uint(0) >> 1))
func SubsliceOffset(data []byte, subslice []byte) int {
datap := (*reflect.SliceHeader)(unsafe.Pointer(&data))
hlp := (*reflect.SliceHeader)(unsafe.Pointer(&subslice))
if hlp.Data < datap.Data {
panic(fmt.Errorf("subslice address (%d) is before data address (%d)", hlp.Data, datap.Data))
}
offset := hlp.Data - datap.Data
if offset > maxInt {
panic(fmt.Errorf("slice offset larger than int (%d)", offset))
}
intoffset := int(offset)
if intoffset > datap.Len {
panic(fmt.Errorf("slice offset (%d) is farther than data length (%d)", intoffset, datap.Len))
}
if intoffset+hlp.Len > datap.Len {
panic(fmt.Errorf("slice ends (%d+%d) is farther than data length (%d)", intoffset, hlp.Len, datap.Len))
}
return intoffset
}
-79
View File
@@ -1,79 +0,0 @@
package unsafe_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/pelletier/go-toml/v2/internal/unsafe"
)
func TestUnsafeSubsliceOffsetValid(t *testing.T) {
examples := []struct {
desc string
test func() ([]byte, []byte)
offset int
}{
{
desc: "simple",
test: func() ([]byte, []byte) {
data := []byte("hello")
return data, data[1:]
},
offset: 1,
},
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
d, s := e.test()
offset := unsafe.SubsliceOffset(d, s)
assert.Equal(t, e.offset, offset)
})
}
}
func TestUnsafeSubsliceOffsetInvalid(t *testing.T) {
examples := []struct {
desc string
test func() ([]byte, []byte)
}{
{
desc: "unrelated arrays",
test: func() ([]byte, []byte) {
return []byte("one"), []byte("two")
},
},
{
desc: "slice starts before data",
test: func() ([]byte, []byte) {
full := []byte("hello world")
return full[5:], full[1:]
},
},
{
desc: "slice starts after data",
test: func() ([]byte, []byte) {
full := []byte("hello world")
return full[:3], full[5:]
},
},
{
desc: "slice ends after data",
test: func() ([]byte, []byte) {
full := []byte("hello world")
return full[:5], full[3:8]
},
},
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
d, s := e.test()
require.Panics(t, func() {
unsafe.SubsliceOffset(d, s)
})
})
}
}
+112
View File
@@ -0,0 +1,112 @@
// Parsing keys handling both bare and quoted keys.
package toml
import (
"errors"
"fmt"
)
// Convert the bare key group string to an array.
// The input supports double quotation and single quotation,
// but escape sequences are not supported. Lexers must unescape them beforehand.
func parseKey(key string) ([]string, error) {
runes := []rune(key)
var groups []string
if len(key) == 0 {
return nil, errors.New("empty key")
}
idx := 0
for idx < len(runes) {
for ; idx < len(runes) && isSpace(runes[idx]); idx++ {
// skip leading whitespace
}
if idx >= len(runes) {
break
}
r := runes[idx]
if isValidBareChar(r) {
// parse bare key
startIdx := idx
endIdx := -1
idx++
for idx < len(runes) {
r = runes[idx]
if isValidBareChar(r) {
idx++
} else if r == '.' {
endIdx = idx
break
} else if isSpace(r) {
endIdx = idx
for ; idx < len(runes) && isSpace(runes[idx]); idx++ {
// skip trailing whitespace
}
if idx < len(runes) && runes[idx] != '.' {
return nil, fmt.Errorf("invalid key character after whitespace: %c", runes[idx])
}
break
} else {
return nil, fmt.Errorf("invalid bare key character: %c", r)
}
}
if endIdx == -1 {
endIdx = idx
}
groups = append(groups, string(runes[startIdx:endIdx]))
} else if r == '\'' {
// parse single quoted key
idx++
startIdx := idx
for {
if idx >= len(runes) {
return nil, fmt.Errorf("unclosed single-quoted key")
}
r = runes[idx]
if r == '\'' {
groups = append(groups, string(runes[startIdx:idx]))
idx++
break
}
idx++
}
} else if r == '"' {
// parse double quoted key
idx++
startIdx := idx
for {
if idx >= len(runes) {
return nil, fmt.Errorf("unclosed double-quoted key")
}
r = runes[idx]
if r == '"' {
groups = append(groups, string(runes[startIdx:idx]))
idx++
break
}
idx++
}
} else if r == '.' {
idx++
if idx >= len(runes) {
return nil, fmt.Errorf("unexpected end of key")
}
r = runes[idx]
if !isValidBareChar(r) && r != '\'' && r != '"' && r != ' ' {
return nil, fmt.Errorf("expecting key part after dot")
}
} else {
return nil, fmt.Errorf("invalid key character: %c", r)
}
}
if len(groups) == 0 {
return nil, fmt.Errorf("empty key")
}
return groups, nil
}
func isValidBareChar(r rune) bool {
return isAlphanumeric(r) || r == '-' || isDigit(r)
}
+79
View File
@@ -0,0 +1,79 @@
package toml
import (
"fmt"
"testing"
)
func testResult(t *testing.T, key string, expected []string) {
parsed, err := parseKey(key)
t.Logf("key=%s expected=%s parsed=%s", key, expected, parsed)
if err != nil {
t.Fatal("Unexpected error:", err)
}
if len(expected) != len(parsed) {
t.Fatal("Expected length", len(expected), "but", len(parsed), "parsed")
}
for index, expectedKey := range expected {
if expectedKey != parsed[index] {
t.Fatal("Expected", expectedKey, "at index", index, "but found", parsed[index])
}
}
}
func testError(t *testing.T, key string, expectedError string) {
res, err := parseKey(key)
if err == nil {
t.Fatalf("Expected error, but successfully parsed key %s", res)
}
if fmt.Sprintf("%s", err) != expectedError {
t.Fatalf("Expected error \"%s\", but got \"%s\".", expectedError, err)
}
}
func TestBareKeyBasic(t *testing.T) {
testResult(t, "test", []string{"test"})
}
func TestBareKeyDotted(t *testing.T) {
testResult(t, "this.is.a.key", []string{"this", "is", "a", "key"})
}
func TestDottedKeyBasic(t *testing.T) {
testResult(t, "\"a.dotted.key\"", []string{"a.dotted.key"})
}
func TestBaseKeyPound(t *testing.T) {
testError(t, "hello#world", "invalid bare key character: #")
}
func TestUnclosedSingleQuotedKey(t *testing.T) {
testError(t, "'", "unclosed single-quoted key")
}
func TestUnclosedDoubleQuotedKey(t *testing.T) {
testError(t, "\"", "unclosed double-quoted key")
}
func TestInvalidStartKeyCharacter(t *testing.T) {
testError(t, "/", "invalid key character: /")
}
func TestInvalidSpaceInKey(t *testing.T) {
testError(t, "invalid key", "invalid key character after whitespace: k")
}
func TestQuotedKeys(t *testing.T) {
testResult(t, `hello."foo".bar`, []string{"hello", "foo", "bar"})
testResult(t, `"hello!"`, []string{"hello!"})
testResult(t, `foo."ba.r".baz`, []string{"foo", "ba.r", "baz"})
// escape sequences must not be converted
testResult(t, `"hello\tworld"`, []string{`hello\tworld`})
}
func TestEmptyKey(t *testing.T) {
testError(t, ``, "empty key")
testError(t, ` `, "empty key")
testResult(t, `""`, []string{""})
}
+1031
View File
File diff suppressed because it is too large Load Diff
+1247
View File
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -1,6 +1,12 @@
// Implementation of TOML's local date/time.
// Copied over from https://github.com/googleapis/google-cloud-go/blob/master/civil/civil.go
// to avoid pulling all the Google dependencies.
//
// Copied over from Google's civil to avoid pulling all the Google dependencies.
// Originals:
// https://raw.githubusercontent.com/googleapis/google-cloud-go/ed46f5086358513cf8c25f8e3f022cb838a49d66/civil/civil.go
// Changes:
// * Renamed files from civil* to localtime*.
// * Package changed from civil to toml.
// * 'Local' prefix added to all structs.
//
// Copyright 2016 Google LLC
//
+10
View File
@@ -1,3 +1,13 @@
// Implementation of TOML's local date/time.
//
// Copied over from Google's civil to avoid pulling all the Google dependencies.
// Originals:
// https://raw.githubusercontent.com/googleapis/google-cloud-go/ed46f5086358513cf8c25f8e3f022cb838a49d66/civil/civil_test.go
// Changes:
// * Renamed files from civil* to localtime*.
// * Package changed from civil to toml.
// * 'Local' prefix added to all structs.
//
// Copyright 2016 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
+1308
View File
File diff suppressed because it is too large Load Diff
+39
View File
@@ -0,0 +1,39 @@
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"]
[[subdocptrs]]
name = "Second"
[basic_map]
one = "one"
two = "two"
[subdoc]
[subdoc.second]
name = "Second"
[subdoc.first]
name = "First"
[basic]
uint = 5001
bool = true
float = 123.4
float64 = 123.456782132399
int = 5000
string = "Bite me"
date = 1979-05-27T07:32:00Z
[[subdoclist]]
name = "List.First"
[[subdoclist]]
name = "List.Second"
+4131
View File
File diff suppressed because it is too large Load Diff
+39
View File
@@ -0,0 +1,39 @@
title = "TOML Marshal Testing"
[basic]
bool = true
date = 1979-05-27T07:32:00Z
float = 123.4
float64 = 123.456782132399
int = 5000
string = "Bite me"
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]
[basic_map]
one = "one"
two = "two"
[subdoc]
[subdoc.first]
name = "First"
[subdoc.second]
name = "Second"
[[subdoclist]]
name = "List.First"
[[subdoclist]]
name = "List.Second"
[[subdocptrs]]
name = "Second"
-661
View File
@@ -1,661 +0,0 @@
package toml
import (
"bytes"
"errors"
"fmt"
"io"
"reflect"
"sort"
"strconv"
"strings"
"time"
)
// Marshal serializes a Go value as a TOML document.
//
// It is a shortcut for Encoder.Encode() with the default options.
func Marshal(v interface{}) ([]byte, error) {
var buf bytes.Buffer
enc := NewEncoder(&buf)
err := enc.Encode(v)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// Encoder writes a TOML document to an output stream.
type Encoder struct {
w io.Writer
}
type encoderCtx struct {
// Current top-level key.
parentKey []string
// Key that should be used for a KV.
key string
// Extra flag to account for the empty string
hasKey bool
// Set to true to indicate that the encoder is inside a KV, so that all
// tables need to be inlined.
insideKv bool
// Set to true to skip the first table header in an array table.
skipTableHeader bool
options valueOptions
}
type valueOptions struct {
multiline bool
}
func (ctx *encoderCtx) shiftKey() {
if ctx.hasKey {
ctx.parentKey = append(ctx.parentKey, ctx.key)
ctx.clearKey()
}
}
func (ctx *encoderCtx) setKey(k string) {
ctx.key = k
ctx.hasKey = true
}
func (ctx *encoderCtx) clearKey() {
ctx.key = ""
ctx.hasKey = false
}
// NewEncoder returns a new Encoder that writes to w.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{
w: w,
}
}
// Encode writes a TOML representation of v to the stream.
//
// If v cannot be represented to TOML it returns an error.
//
// Encoding rules:
//
// 1. A top level slice containing only maps or structs is encoded as [[table
// array]].
//
// 2. All slices not matching rule 1 are encoded as [array]. As a result, any
// map or struct they contain is encoded as an {inline table}.
//
// 3. Nil interfaces and nil pointers are not supported.
//
// 4. Keys in key-values always have one part.
//
// 5. Intermediate tables are always printed.
//
// By default, strings are encoded as literal string, unless they contain either
// a newline character or a single quote. In that case they are emited as quoted
// strings.
//
// When encoding structs, fields are encoded in order of definition, with their
// exact name. The following struct tags are available:
//
// `toml:"foo"`: changes the name of the key to use for the field to foo.
//
// `multiline:"true"`: when the field contains a string, it will be emitted as
// a quoted multi-line TOML string.
func (enc *Encoder) Encode(v interface{}) error {
var b []byte
var ctx encoderCtx
b, err := enc.encode(b, ctx, reflect.ValueOf(v))
if err != nil {
return err
}
_, err = enc.w.Write(b)
return err
}
func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
switch i := v.Interface().(type) {
case time.Time: // TODO: add TextMarshaler
b = i.AppendFormat(b, time.RFC3339)
return b, nil
}
// containers
switch v.Kind() {
case reflect.Map:
return enc.encodeMap(b, ctx, v)
case reflect.Struct:
return enc.encodeStruct(b, ctx, v)
case reflect.Slice:
return enc.encodeSlice(b, ctx, v)
case reflect.Interface:
if v.IsNil() {
return nil, errNilInterface
}
return enc.encode(b, ctx, v.Elem())
case reflect.Ptr:
if v.IsNil() {
return enc.encode(b, ctx, reflect.Zero(v.Type().Elem()))
}
return enc.encode(b, ctx, v.Elem())
}
// values
var err error
switch v.Kind() {
case reflect.String:
b, err = enc.encodeString(b, v.String(), ctx.options)
case reflect.Float32:
b = strconv.AppendFloat(b, v.Float(), 'f', -1, 32)
case reflect.Float64:
b = strconv.AppendFloat(b, v.Float(), 'f', -1, 64)
case reflect.Bool:
if v.Bool() {
b = append(b, "true"...)
} else {
b = append(b, "false"...)
}
case reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint:
b = strconv.AppendUint(b, v.Uint(), 10)
case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int:
b = strconv.AppendInt(b, v.Int(), 10)
default:
err = fmt.Errorf("unsupported encode value kind: %s", v.Kind())
}
if err != nil {
return nil, err
}
return b, nil
}
func isNil(v reflect.Value) bool {
switch v.Kind() {
case reflect.Ptr, reflect.Interface, reflect.Map:
return v.IsNil()
default:
return false
}
}
func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v reflect.Value) ([]byte, error) {
var err error
if !ctx.hasKey {
panic("caller of encodeKv should have set the key in the context")
}
if isNil(v) {
return b, nil
}
b, err = enc.encodeKey(b, ctx.key)
if err != nil {
return nil, err
}
b = append(b, " = "...)
// create a copy of the context because the value of a KV shouldn't
// modify the global context.
subctx := ctx
subctx.insideKv = true
subctx.shiftKey()
subctx.options = options
b, err = enc.encode(b, subctx, v)
if err != nil {
return nil, err
}
return b, nil
}
const literalQuote = '\''
func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) ([]byte, error) {
if needsQuoting(v) {
b = enc.encodeQuotedString(options.multiline, b, v)
} else {
b = enc.encodeLiteralString(b, v)
}
return b, nil
}
func needsQuoting(v string) bool {
return strings.ContainsAny(v, "'\b\f\n\r\t")
}
// caller should have checked that the string does not contain new lines or '
func (enc *Encoder) encodeLiteralString(b []byte, v string) []byte {
b = append(b, literalQuote)
b = append(b, v...)
b = append(b, literalQuote)
return b
}
func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byte {
const hextable = "0123456789ABCDEF"
stringQuote := `"`
if multiline {
stringQuote = `"""`
}
b = append(b, stringQuote...)
if multiline {
b = append(b, '\n')
}
for _, r := range []byte(v) {
switch r {
case '\\':
b = append(b, `\\`...)
case '"':
b = append(b, `\"`...)
case '\b':
b = append(b, `\b`...)
case '\f':
b = append(b, `\f`...)
case '\n':
if multiline {
b = append(b, r)
} else {
b = append(b, `\n`...)
}
case '\r':
b = append(b, `\r`...)
case '\t':
b = append(b, `\t`...)
default:
switch {
case r >= 0x0 && r <= 0x8, r >= 0xA && r <= 0x1F, r == 0x7F:
b = append(b, `\u00`...)
b = append(b, hextable[r>>4])
b = append(b, hextable[r&0x0f])
default:
b = append(b, r)
}
}
// U+0000 to U+0008, U+000A to U+001F, U+007F
}
b = append(b, stringQuote...)
return b
}
// called should have checked that the string is in A-Z / a-z / 0-9 / - / _
func (enc *Encoder) encodeUnquotedKey(b []byte, v string) []byte {
return append(b, v...)
}
func (enc *Encoder) encodeTableHeader(b []byte, key []string) ([]byte, error) {
if len(key) == 0 {
return b, nil
}
b = append(b, '[')
var err error
b, err = enc.encodeKey(b, key[0])
if err != nil {
return nil, err
}
for _, k := range key[1:] {
b = append(b, '.')
b, err = enc.encodeKey(b, k)
if err != nil {
return nil, err
}
}
b = append(b, "]\n"...)
return b, nil
}
func (enc *Encoder) encodeKey(b []byte, k string) ([]byte, error) {
needsQuotation := false
cannotUseLiteral := false
for _, c := range k {
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' {
continue
}
if c == '\n' {
return nil, fmt.Errorf("TOML does not support multiline keys")
}
if c == literalQuote {
cannotUseLiteral = true
}
needsQuotation = true
}
if cannotUseLiteral {
b = enc.encodeQuotedString(false, b, k)
} else if needsQuotation {
b = enc.encodeLiteralString(b, k)
} else {
b = enc.encodeUnquotedKey(b, k)
}
return b, nil
}
func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
if v.Type().Key().Kind() != reflect.String {
return nil, fmt.Errorf("type '%s' not supported as map key", v.Type().Key().Kind())
}
t := table{}
iter := v.MapRange()
for iter.Next() {
k := iter.Key().String()
v := iter.Value()
if isNil(v) {
continue
}
table, err := willConvertToTableOrArrayTable(v)
if err != nil {
return nil, err
}
if table {
t.pushTable(k, v, valueOptions{})
} else {
t.pushKV(k, v, valueOptions{})
}
}
sortEntriesByKey(t.kvs)
sortEntriesByKey(t.tables)
return enc.encodeTable(b, ctx, t)
}
func sortEntriesByKey(e []entry) {
sort.Slice(e, func(i, j int) bool {
return e[i].Key < e[j].Key
})
}
type entry struct {
Key string
Value reflect.Value
Options valueOptions
}
type table struct {
kvs []entry
tables []entry
}
func (t *table) pushKV(k string, v reflect.Value, options valueOptions) {
t.kvs = append(t.kvs, entry{Key: k, Value: v, Options: options})
}
func (t *table) pushTable(k string, v reflect.Value, options valueOptions) {
t.tables = append(t.tables, entry{Key: k, Value: v, Options: options})
}
func (t *table) hasKVs() bool {
return len(t.kvs) > 0
}
func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
t := table{}
// TODO: cache this?
typ := v.Type()
for i := 0; i < typ.NumField(); i++ {
fieldType := typ.Field(i)
// only consider exported fields
if fieldType.PkgPath != "" {
continue
}
k, ok := fieldType.Tag.Lookup("toml")
if !ok {
k = fieldType.Name
}
// special field name to skip field
if k == "-" {
continue
}
f := v.Field(i)
if isNil(f) {
continue
}
willConvert, err := willConvertToTableOrArrayTable(f)
if err != nil {
return nil, err
}
options := valueOptions{}
ml, ok := fieldType.Tag.Lookup("multiline")
if ok {
options.multiline = ml == "true"
}
if willConvert {
t.pushTable(k, f, options)
} else {
t.pushKV(k, f, options)
}
}
return enc.encodeTable(b, ctx, t)
}
func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, error) {
var err error
ctx.shiftKey()
if ctx.insideKv {
b = append(b, '{')
first := true
for _, kv := range t.kvs {
if first {
first = false
} else {
b = append(b, `, `...)
}
ctx.setKey(kv.Key)
b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value)
if err != nil {
return nil, err
}
}
for _, table := range t.tables {
if first {
first = false
} else {
b = append(b, `, `...)
}
ctx.setKey(table.Key)
b, err = enc.encode(b, ctx, table.Value)
if err != nil {
return nil, err
}
b = append(b, '\n')
}
b = append(b, "}\n"...)
return b, nil
}
if !ctx.skipTableHeader {
b, err = enc.encodeTableHeader(b, ctx.parentKey)
if err != nil {
return nil, err
}
}
ctx.skipTableHeader = false
for _, kv := range t.kvs {
ctx.setKey(kv.Key)
b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value)
if err != nil {
return nil, err
}
b = append(b, '\n')
}
for _, table := range t.tables {
ctx.setKey(table.Key)
b, err = enc.encode(b, ctx, table.Value)
if err != nil {
return nil, err
}
b = append(b, '\n')
}
return b, nil
}
var errNilInterface = errors.New("nil interface not supported")
var errNilPointer = errors.New("nil pointer not supported")
func willConvertToTable(v reflect.Value) (bool, error) {
switch v.Interface().(type) {
case time.Time: // TODO: add TextMarshaler
return false, nil
}
t := v.Type()
switch t.Kind() {
case reflect.Map, reflect.Struct:
return true, nil
case reflect.Interface:
if v.IsNil() {
return false, errNilInterface
}
return willConvertToTable(v.Elem())
case reflect.Ptr:
if v.IsNil() {
return false, nil
}
return willConvertToTable(v.Elem())
default:
return false, nil
}
}
func willConvertToTableOrArrayTable(v reflect.Value) (bool, error) {
t := v.Type()
if t.Kind() == reflect.Interface {
if v.IsNil() {
return false, errNilInterface
}
return willConvertToTableOrArrayTable(v.Elem())
}
if t.Kind() == reflect.Slice {
if v.Len() == 0 {
// An empty slice should be a kv = [].
return false, nil
}
for i := 0; i < v.Len(); i++ {
t, err := willConvertToTable(v.Index(i))
if err != nil {
return false, err
}
if !t {
return false, nil
}
}
return true, nil
}
return willConvertToTable(v)
}
func (enc *Encoder) encodeSlice(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
if v.Len() == 0 {
b = append(b, "[]"...)
return b, nil
}
allTables, err := willConvertToTableOrArrayTable(v)
if err != nil {
return nil, err
}
if allTables {
return enc.encodeSliceAsArrayTable(b, ctx, v)
}
return enc.encodeSliceAsArray(b, ctx, v)
}
// caller should have checked that v is a slice that only contains values that
// encode into tables.
func (enc *Encoder) encodeSliceAsArrayTable(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
if v.Len() == 0 {
return b, nil
}
ctx.shiftKey()
var err error
scratch := make([]byte, 0, 64)
scratch = append(scratch, "[["...)
for i, k := range ctx.parentKey {
if i > 0 {
scratch = append(scratch, '.')
}
scratch, err = enc.encodeKey(scratch, k)
if err != nil {
return nil, err
}
}
scratch = append(scratch, "]]\n"...)
ctx.skipTableHeader = true
for i := 0; i < v.Len(); i++ {
b = append(b, scratch...)
b, err = enc.encode(b, ctx, v.Index(i))
if err != nil {
return nil, err
}
}
return b, nil
}
func (enc *Encoder) encodeSliceAsArray(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
b = append(b, '[')
var err error
first := true
for i := 0; i < v.Len(); i++ {
if !first {
b = append(b, ", "...)
}
first = false
b, err = enc.encode(b, ctx, v.Index(i))
if err != nil {
return nil, err
}
}
b = append(b, ']')
return b, nil
}
-275
View File
@@ -1,275 +0,0 @@
package toml_test
import (
"bytes"
"encoding/json"
"strings"
"testing"
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMarshal(t *testing.T) {
examples := []struct {
desc string
v interface{}
expected string
err bool
}{
{
desc: "simple map and string",
v: map[string]string{
"hello": "world",
},
expected: "hello = 'world'",
},
{
desc: "map with new line in key",
v: map[string]string{
"hel\nlo": "world",
},
err: true,
},
{
desc: `map with " in key`,
v: map[string]string{
`hel"lo`: "world",
},
expected: `'hel"lo' = 'world'`,
},
{
desc: "map in map and string",
v: map[string]map[string]string{
"table": {
"hello": "world",
},
},
expected: `
[table]
hello = 'world'`,
},
{
desc: "map in map in map and string",
v: map[string]map[string]map[string]string{
"this": {
"is": {
"a": "test",
},
},
},
expected: `
[this]
[this.is]
a = 'test'`,
},
{
// TODO: this test is flaky because output changes depending on
// the map iteration order.
desc: "map in map in map and string with values",
v: map[string]interface{}{
"this": map[string]interface{}{
"is": map[string]string{
"a": "test",
},
"also": "that",
},
},
expected: `
[this]
also = 'that'
[this.is]
a = 'test'`,
},
{
desc: "simple string array",
v: map[string][]string{
"array": {"one", "two", "three"},
},
expected: `array = ['one', 'two', 'three']`,
},
{
desc: "nested string arrays",
v: map[string][][]string{
"array": {{"one", "two"}, {"three"}},
},
expected: `array = [['one', 'two'], ['three']]`,
},
{
desc: "mixed strings and nested string arrays",
v: map[string][]interface{}{
"array": {"a string", []string{"one", "two"}, "last"},
},
expected: `array = ['a string', ['one', 'two'], 'last']`,
},
{
desc: "slice of maps",
v: map[string][]map[string]string{
"top": {
{"map1.1": "v1.1"},
{"map2.1": "v2.1"},
},
},
expected: `
[[top]]
'map1.1' = 'v1.1'
[[top]]
'map2.1' = 'v2.1'
`,
},
{
desc: "map with two keys",
v: map[string]string{
"key1": "value1",
"key2": "value2",
},
expected: `
key1 = 'value1'
key2 = 'value2'`,
},
{
desc: "simple struct",
v: struct {
A string
}{
A: "foo",
},
expected: `A = 'foo'`,
},
{
desc: "one level of structs within structs",
v: struct {
A interface{}
}{
A: struct {
K1 string
K2 string
}{
K1: "v1",
K2: "v2",
},
},
expected: `
[A]
K1 = 'v1'
K2 = 'v2'
`,
},
{
desc: "structs in slice with interfaces",
v: map[string]interface{}{
"root": map[string]interface{}{
"nested": []interface{}{
map[string]interface{}{"name": "Bob"},
map[string]interface{}{"name": "Alice"},
},
},
},
expected: `
[root]
[[root.nested]]
name = 'Bob'
[[root.nested]]
name = 'Alice'
`,
},
{
desc: "string escapes",
v: map[string]interface{}{
"a": `'"\`,
},
expected: `a = "'\"\\"`,
},
{
desc: "string utf8 low",
v: map[string]interface{}{
"a": "'Ę",
},
expected: `a = "'Ę"`,
},
{
desc: "string utf8 low 2",
v: map[string]interface{}{
"a": "'\u10A85",
},
expected: "a = \"'\u10A85\"",
},
{
desc: "string utf8 low 2",
v: map[string]interface{}{
"a": "'\u10A85",
},
expected: "a = \"'\u10A85\"",
},
{
desc: "emoji",
v: map[string]interface{}{
"a": "'😀",
},
expected: "a = \"'😀\"",
},
{
desc: "control char",
v: map[string]interface{}{
"a": "'\u001A",
},
expected: `a = "'\u001A"`,
},
{
desc: "multi-line string",
v: map[string]interface{}{
"a": "hello\nworld",
},
expected: `a = "hello\nworld"`,
},
{
desc: "multi-line forced",
v: struct {
A string `multiline:"true"`
}{
A: "hello\nworld",
},
expected: `A = """
hello
world"""`,
},
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
b, err := toml.Marshal(e.v)
if e.err {
require.Error(t, err)
} else {
require.NoError(t, err)
equalStringsIgnoreNewlines(t, e.expected, string(b))
}
})
}
}
func equalStringsIgnoreNewlines(t *testing.T, expected string, actual string) {
t.Helper()
cutset := "\n"
assert.Equal(t, strings.Trim(expected, cutset), strings.Trim(actual, cutset))
}
func TestIssue436(t *testing.T) {
data := []byte(`{"a": [ { "b": { "c": "d" } } ]}`)
var v interface{}
err := json.Unmarshal(data, &v)
require.NoError(t, err)
var buf bytes.Buffer
err = toml.NewEncoder(&buf).Encode(v)
require.NoError(t, err)
expected := `
[[a]]
[a.b]
c = 'd'
`
equalStringsIgnoreNewlines(t, expected, buf.String())
}
+430 -843
View File
File diff suppressed because it is too large Load Diff
+1160 -385
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
// Position support for go-toml
package toml
import (
"fmt"
)
// Position of a document element within a TOML document.
//
// Line and Col are both 1-indexed positions for the element's line number and
// column number, respectively. Values of zero or less will cause Invalid(),
// to return true.
type Position struct {
Line int // line within the document
Col int // column within the line
}
// String representation of the position.
// Displays 1-indexed line and column numbers.
func (p Position) String() string {
return fmt.Sprintf("(%d, %d)", p.Line, p.Col)
}
// Invalid returns whether or not the position is valid (i.e. with negative or
// null values)
func (p Position) Invalid() bool {
return p.Line <= 0 || p.Col <= 0
}
+29
View File
@@ -0,0 +1,29 @@
// Testing support for go-toml
package toml
import (
"testing"
)
func TestPositionString(t *testing.T) {
p := Position{123, 456}
expected := "(123, 456)"
value := p.String()
if value != expected {
t.Errorf("Expected %v, got %v instead", expected, value)
}
}
func TestInvalid(t *testing.T) {
for i, v := range []Position{
{0, 1234},
{1234, 0},
{0, 0},
} {
if !v.Invalid() {
t.Errorf("Position at %v is valid: %v", i, v)
}
}
}
+201
View File
@@ -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)
```
+173
View File
@@ -0,0 +1,173 @@
// 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.
//
// result, err := query.CompileAndExecute("$.foo.bar.baz", tree)
//
// This is roughly equivalent to:
//
// 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.
//
// // 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.
//
// // 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.
//
// // 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::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[::]", 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.
//
// // 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.
//
// // 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.
//
// // 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.
//
// // 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)
//
package query
+357
View File
@@ -0,0 +1,357 @@
// TOML JSONPath lexer.
//
// Written using the principles developed by Rob Pike in
// http://www.youtube.com/watch?v=HxaD_trXwRE
package query
import (
"fmt"
"github.com/pelletier/go-toml"
"strconv"
"strings"
"unicode/utf8"
)
// Lexer state function
type queryLexStateFn func() queryLexStateFn
// Lexer definition
type queryLexer struct {
input string
start int
pos int
width int
tokens chan token
depth int
line int
col int
stringTerm string
}
func (l *queryLexer) run() {
for state := l.lexVoid; state != nil; {
state = state()
}
close(l.tokens)
}
func (l *queryLexer) nextStart() {
// iterate by runes (utf8 characters)
// search for newlines and advance line/col counts
for i := l.start; i < l.pos; {
r, width := utf8.DecodeRuneInString(l.input[i:])
if r == '\n' {
l.line++
l.col = 1
} else {
l.col++
}
i += width
}
// advance start position to next token
l.start = l.pos
}
func (l *queryLexer) emit(t tokenType) {
l.tokens <- token{
Position: toml.Position{Line: l.line, Col: l.col},
typ: t,
val: l.input[l.start:l.pos],
}
l.nextStart()
}
func (l *queryLexer) emitWithValue(t tokenType, value string) {
l.tokens <- token{
Position: toml.Position{Line: l.line, Col: l.col},
typ: t,
val: value,
}
l.nextStart()
}
func (l *queryLexer) next() rune {
if l.pos >= len(l.input) {
l.width = 0
return eof
}
var r rune
r, l.width = utf8.DecodeRuneInString(l.input[l.pos:])
l.pos += l.width
return r
}
func (l *queryLexer) ignore() {
l.nextStart()
}
func (l *queryLexer) backup() {
l.pos -= l.width
}
func (l *queryLexer) errorf(format string, args ...interface{}) queryLexStateFn {
l.tokens <- token{
Position: toml.Position{Line: l.line, Col: l.col},
typ: tokenError,
val: fmt.Sprintf(format, args...),
}
return nil
}
func (l *queryLexer) peek() rune {
r := l.next()
l.backup()
return r
}
func (l *queryLexer) accept(valid string) bool {
if strings.ContainsRune(valid, l.next()) {
return true
}
l.backup()
return false
}
func (l *queryLexer) follow(next string) bool {
return strings.HasPrefix(l.input[l.pos:], next)
}
func (l *queryLexer) lexVoid() queryLexStateFn {
for {
next := l.peek()
switch next {
case '$':
l.pos++
l.emit(tokenDollar)
continue
case '.':
if l.follow("..") {
l.pos += 2
l.emit(tokenDotDot)
} else {
l.pos++
l.emit(tokenDot)
}
continue
case '[':
l.pos++
l.emit(tokenLeftBracket)
continue
case ']':
l.pos++
l.emit(tokenRightBracket)
continue
case ',':
l.pos++
l.emit(tokenComma)
continue
case '*':
l.pos++
l.emit(tokenStar)
continue
case '(':
l.pos++
l.emit(tokenLeftParen)
continue
case ')':
l.pos++
l.emit(tokenRightParen)
continue
case '?':
l.pos++
l.emit(tokenQuestion)
continue
case ':':
l.pos++
l.emit(tokenColon)
continue
case '\'':
l.ignore()
l.stringTerm = string(next)
return l.lexString
case '"':
l.ignore()
l.stringTerm = string(next)
return l.lexString
}
if isSpace(next) {
l.next()
l.ignore()
continue
}
if isAlphanumeric(next) {
return l.lexKey
}
if next == '+' || next == '-' || isDigit(next) {
return l.lexNumber
}
if l.next() == eof {
break
}
return l.errorf("unexpected char: '%v'", next)
}
l.emit(tokenEOF)
return nil
}
func (l *queryLexer) lexKey() queryLexStateFn {
for {
next := l.peek()
if !isAlphanumeric(next) {
l.emit(tokenKey)
return l.lexVoid
}
if l.next() == eof {
break
}
}
l.emit(tokenEOF)
return nil
}
func (l *queryLexer) lexString() queryLexStateFn {
l.pos++
l.ignore()
growingString := ""
for {
if l.follow(l.stringTerm) {
l.emitWithValue(tokenString, growingString)
l.pos++
l.ignore()
return l.lexVoid
}
if l.follow("\\\"") {
l.pos++
growingString += "\""
} else if l.follow("\\'") {
l.pos++
growingString += "'"
} else if l.follow("\\n") {
l.pos++
growingString += "\n"
} else if l.follow("\\b") {
l.pos++
growingString += "\b"
} else if l.follow("\\f") {
l.pos++
growingString += "\f"
} else if l.follow("\\/") {
l.pos++
growingString += "/"
} else if l.follow("\\t") {
l.pos++
growingString += "\t"
} else if l.follow("\\r") {
l.pos++
growingString += "\r"
} else if l.follow("\\\\") {
l.pos++
growingString += "\\"
} else if l.follow("\\u") {
l.pos += 2
code := ""
for i := 0; i < 4; i++ {
c := l.peek()
l.pos++
if !isHexDigit(c) {
return l.errorf("unfinished unicode escape")
}
code = code + string(c)
}
l.pos--
intcode, err := strconv.ParseInt(code, 16, 32)
if err != nil {
return l.errorf("invalid unicode escape: \\u" + code)
}
growingString += string(rune(intcode))
} else if l.follow("\\U") {
l.pos += 2
code := ""
for i := 0; i < 8; i++ {
c := l.peek()
l.pos++
if !isHexDigit(c) {
return l.errorf("unfinished unicode escape")
}
code = code + string(c)
}
l.pos--
intcode, err := strconv.ParseInt(code, 16, 32)
if err != nil {
return l.errorf("invalid unicode escape: \\u" + code)
}
growingString += string(rune(intcode))
} else if l.follow("\\") {
l.pos++
return l.errorf("invalid escape sequence: \\" + string(l.peek()))
} else {
growingString += string(l.peek())
}
if l.next() == eof {
break
}
}
return l.errorf("unclosed string")
}
func (l *queryLexer) lexNumber() queryLexStateFn {
l.ignore()
if !l.accept("+") {
l.accept("-")
}
pointSeen := false
digitSeen := false
for {
next := l.next()
if next == '.' {
if pointSeen {
return l.errorf("cannot have two dots in one float")
}
if !isDigit(l.peek()) {
return l.errorf("float cannot end with a dot")
}
pointSeen = true
} else if isDigit(next) {
digitSeen = true
} else {
l.backup()
break
}
if pointSeen && !digitSeen {
return l.errorf("cannot start float with a dot")
}
}
if !digitSeen {
return l.errorf("no digit in that number")
}
if pointSeen {
l.emit(tokenFloat)
} else {
l.emit(tokenInteger)
}
return l.lexVoid
}
// Entry point
func lexQuery(input string) chan token {
l := &queryLexer{
input: input,
tokens: make(chan token),
line: 1,
col: 1,
}
go l.run()
return l.tokens
}
+179
View File
@@ -0,0 +1,179 @@
package query
import (
"github.com/pelletier/go-toml"
"testing"
)
func testQLFlow(t *testing.T, input string, expectedFlow []token) {
ch := lexQuery(input)
for idx, expected := range expectedFlow {
token := <-ch
if token != expected {
t.Log("While testing #", idx, ":", input)
t.Log("compared (got)", token, "to (expected)", expected)
t.Log("\tvalue:", token.val, "<->", expected.val)
t.Log("\tvalue as bytes:", []byte(token.val), "<->", []byte(expected.val))
t.Log("\ttype:", token.typ.String(), "<->", expected.typ.String())
t.Log("\tline:", token.Line, "<->", expected.Line)
t.Log("\tcolumn:", token.Col, "<->", expected.Col)
t.Log("compared", token, "to", expected)
t.FailNow()
}
}
tok, ok := <-ch
if ok {
t.Log("channel is not closed!")
t.Log(len(ch)+1, "tokens remaining:")
t.Log("token ->", tok)
for token := range ch {
t.Log("token ->", token)
}
t.FailNow()
}
}
func TestLexSpecialChars(t *testing.T) {
testQLFlow(t, " .$[]..()?*", []token{
{toml.Position{1, 2}, tokenDot, "."},
{toml.Position{1, 3}, tokenDollar, "$"},
{toml.Position{1, 4}, tokenLeftBracket, "["},
{toml.Position{1, 5}, tokenRightBracket, "]"},
{toml.Position{1, 6}, tokenDotDot, ".."},
{toml.Position{1, 8}, tokenLeftParen, "("},
{toml.Position{1, 9}, tokenRightParen, ")"},
{toml.Position{1, 10}, tokenQuestion, "?"},
{toml.Position{1, 11}, tokenStar, "*"},
{toml.Position{1, 12}, tokenEOF, ""},
})
}
func TestLexString(t *testing.T) {
testQLFlow(t, "'foo\n'", []token{
{toml.Position{1, 2}, tokenString, "foo\n"},
{toml.Position{2, 2}, tokenEOF, ""},
})
}
func TestLexDoubleString(t *testing.T) {
testQLFlow(t, `"bar"`, []token{
{toml.Position{1, 2}, tokenString, "bar"},
{toml.Position{1, 6}, tokenEOF, ""},
})
}
func TestLexStringEscapes(t *testing.T) {
testQLFlow(t, `"foo \" \' \b \f \/ \t \r \\ \u03A9 \U00012345 \n bar"`, []token{
{toml.Position{1, 2}, tokenString, "foo \" ' \b \f / \t \r \\ \u03A9 \U00012345 \n bar"},
{toml.Position{1, 55}, tokenEOF, ""},
})
}
func TestLexStringUnfinishedUnicode4(t *testing.T) {
testQLFlow(t, `"\u000"`, []token{
{toml.Position{1, 2}, tokenError, "unfinished unicode escape"},
})
}
func TestLexStringUnfinishedUnicode8(t *testing.T) {
testQLFlow(t, `"\U0000"`, []token{
{toml.Position{1, 2}, tokenError, "unfinished unicode escape"},
})
}
func TestLexStringInvalidEscape(t *testing.T) {
testQLFlow(t, `"\x"`, []token{
{toml.Position{1, 2}, tokenError, "invalid escape sequence: \\x"},
})
}
func TestLexStringUnfinished(t *testing.T) {
testQLFlow(t, `"bar`, []token{
{toml.Position{1, 2}, tokenError, "unclosed string"},
})
}
func TestLexKey(t *testing.T) {
testQLFlow(t, "foo", []token{
{toml.Position{1, 1}, tokenKey, "foo"},
{toml.Position{1, 4}, tokenEOF, ""},
})
}
func TestLexRecurse(t *testing.T) {
testQLFlow(t, "$..*", []token{
{toml.Position{1, 1}, tokenDollar, "$"},
{toml.Position{1, 2}, tokenDotDot, ".."},
{toml.Position{1, 4}, tokenStar, "*"},
{toml.Position{1, 5}, tokenEOF, ""},
})
}
func TestLexBracketKey(t *testing.T) {
testQLFlow(t, "$[foo]", []token{
{toml.Position{1, 1}, tokenDollar, "$"},
{toml.Position{1, 2}, tokenLeftBracket, "["},
{toml.Position{1, 3}, tokenKey, "foo"},
{toml.Position{1, 6}, tokenRightBracket, "]"},
{toml.Position{1, 7}, tokenEOF, ""},
})
}
func TestLexSpace(t *testing.T) {
testQLFlow(t, "foo bar baz", []token{
{toml.Position{1, 1}, tokenKey, "foo"},
{toml.Position{1, 5}, tokenKey, "bar"},
{toml.Position{1, 9}, tokenKey, "baz"},
{toml.Position{1, 12}, tokenEOF, ""},
})
}
func TestLexInteger(t *testing.T) {
testQLFlow(t, "100 +200 -300", []token{
{toml.Position{1, 1}, tokenInteger, "100"},
{toml.Position{1, 5}, tokenInteger, "+200"},
{toml.Position{1, 10}, tokenInteger, "-300"},
{toml.Position{1, 14}, tokenEOF, ""},
})
}
func TestLexFloat(t *testing.T) {
testQLFlow(t, "100.0 +200.0 -300.0", []token{
{toml.Position{1, 1}, tokenFloat, "100.0"},
{toml.Position{1, 7}, tokenFloat, "+200.0"},
{toml.Position{1, 14}, tokenFloat, "-300.0"},
{toml.Position{1, 20}, tokenEOF, ""},
})
}
func TestLexFloatWithMultipleDots(t *testing.T) {
testQLFlow(t, "4.2.", []token{
{toml.Position{1, 1}, tokenError, "cannot have two dots in one float"},
})
}
func TestLexFloatLeadingDot(t *testing.T) {
testQLFlow(t, "+.1", []token{
{toml.Position{1, 1}, tokenError, "cannot start float with a dot"},
})
}
func TestLexFloatWithTrailingDot(t *testing.T) {
testQLFlow(t, "42.", []token{
{toml.Position{1, 1}, tokenError, "float cannot end with a dot"},
})
}
func TestLexNumberWithoutDigit(t *testing.T) {
testQLFlow(t, "+", []token{
{toml.Position{1, 1}, tokenError, "no digit in that number"},
})
}
func TestLexUnknown(t *testing.T) {
testQLFlow(t, "^", []token{
{toml.Position{1, 1}, tokenError, "unexpected char: '94'"},
})
}
+311
View File
@@ -0,0 +1,311 @@
package query
import (
"fmt"
"reflect"
"github.com/pelletier/go-toml"
)
// base match
type matchBase struct {
next pathFn
}
func (f *matchBase) setNext(next pathFn) {
f.next = next
}
// terminating functor - gathers results
type terminatingFn struct {
// empty
}
func newTerminatingFn() *terminatingFn {
return &terminatingFn{}
}
func (f *terminatingFn) setNext(next pathFn) {
// do nothing
}
func (f *terminatingFn) call(node interface{}, ctx *queryContext) {
ctx.result.appendResult(node, ctx.lastPosition)
}
// match single key
type matchKeyFn struct {
matchBase
Name string
}
func newMatchKeyFn(name string) *matchKeyFn {
return &matchKeyFn{Name: name}
}
func (f *matchKeyFn) call(node interface{}, ctx *queryContext) {
if array, ok := node.([]*toml.Tree); ok {
for _, tree := range array {
item := tree.GetPath([]string{f.Name})
if item != nil {
ctx.lastPosition = tree.GetPositionPath([]string{f.Name})
f.next.call(item, ctx)
}
}
} else if tree, ok := node.(*toml.Tree); ok {
item := tree.GetPath([]string{f.Name})
if item != nil {
ctx.lastPosition = tree.GetPositionPath([]string{f.Name})
f.next.call(item, ctx)
}
}
}
// match single index
type matchIndexFn struct {
matchBase
Idx int
}
func newMatchIndexFn(idx int) *matchIndexFn {
return &matchIndexFn{Idx: idx}
}
func (f *matchIndexFn) call(node interface{}, ctx *queryContext) {
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
}
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) {
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()
}
// 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
type matchAnyFn struct {
matchBase
}
func newMatchAnyFn() *matchAnyFn {
return &matchAnyFn{}
}
func (f *matchAnyFn) call(node interface{}, ctx *queryContext) {
if tree, ok := node.(*toml.Tree); ok {
for _, k := range tree.Keys() {
v := tree.GetPath([]string{k})
ctx.lastPosition = tree.GetPositionPath([]string{k})
f.next.call(v, ctx)
}
}
}
// filter through union
type matchUnionFn struct {
Union []pathFn
}
func (f *matchUnionFn) setNext(next pathFn) {
for _, fn := range f.Union {
fn.setNext(next)
}
}
func (f *matchUnionFn) call(node interface{}, ctx *queryContext) {
for _, fn := range f.Union {
fn.call(node, ctx)
}
}
// match every single last node in the tree
type matchRecursiveFn struct {
matchBase
}
func newMatchRecursiveFn() *matchRecursiveFn {
return &matchRecursiveFn{}
}
func (f *matchRecursiveFn) call(node interface{}, ctx *queryContext) {
originalPosition := ctx.lastPosition
if tree, ok := node.(*toml.Tree); ok {
var visit func(tree *toml.Tree)
visit = func(tree *toml.Tree) {
for _, k := range tree.Keys() {
v := tree.GetPath([]string{k})
ctx.lastPosition = tree.GetPositionPath([]string{k})
f.next.call(v, ctx)
switch node := v.(type) {
case *toml.Tree:
visit(node)
case []*toml.Tree:
for _, subtree := range node {
visit(subtree)
}
}
}
}
ctx.lastPosition = originalPosition
f.next.call(tree, ctx)
visit(tree)
}
}
// match based on an externally provided functional filter
type matchFilterFn struct {
matchBase
Pos toml.Position
Name string
}
func newMatchFilterFn(name string, pos toml.Position) *matchFilterFn {
return &matchFilterFn{Name: name, Pos: pos}
}
func (f *matchFilterFn) call(node interface{}, ctx *queryContext) {
fn, ok := (*ctx.filters)[f.Name]
if !ok {
panic(fmt.Sprintf("%s: query context does not have filter '%s'",
f.Pos.String(), f.Name))
}
switch castNode := node.(type) {
case *toml.Tree:
for _, k := range castNode.Keys() {
v := castNode.GetPath([]string{k})
if fn(v) {
ctx.lastPosition = castNode.GetPositionPath([]string{k})
f.next.call(v, ctx)
}
}
case []*toml.Tree:
for _, v := range castNode {
if fn(v) {
if len(castNode) > 0 {
ctx.lastPosition = castNode[0].Position()
}
f.next.call(v, ctx)
}
}
case []interface{}:
for _, v := range castNode {
if fn(v) {
f.next.call(v, ctx)
}
}
}
}
+213
View File
@@ -0,0 +1,213 @@
package query
import (
"fmt"
"strconv"
"testing"
"github.com/pelletier/go-toml"
)
// dump path tree to a string
func pathString(root pathFn) string {
result := fmt.Sprintf("%T:", root)
switch fn := root.(type) {
case *terminatingFn:
result += "{}"
case *matchKeyFn:
result += fmt.Sprintf("{%s}", fn.Name)
result += pathString(fn.next)
case *matchIndexFn:
result += fmt.Sprintf("{%d}", fn.Idx)
result += pathString(fn.next)
case *matchSliceFn:
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 += "{}"
result += pathString(fn.next)
case *matchUnionFn:
result += "{["
for _, v := range fn.Union {
result += pathString(v) + ", "
}
result += "]}"
case *matchRecursiveFn:
result += "{}"
result += pathString(fn.next)
case *matchFilterFn:
result += fmt.Sprintf("{%s}", fn.Name)
result += pathString(fn.next)
}
return result
}
func assertPathMatch(t *testing.T, path, ref *Query) bool {
pathStr := pathString(path.root)
refStr := pathString(ref.root)
if pathStr != refStr {
t.Errorf("paths do not match")
t.Log("test:", pathStr)
t.Log("ref: ", refStr)
return false
}
return true
}
func assertPath(t *testing.T, query string, ref *Query) {
path, _ := parseQuery(lexQuery(query))
assertPathMatch(t, path, ref)
}
func buildPath(parts ...pathFn) *Query {
query := newQuery()
for _, v := range parts {
query.appendPath(v)
}
return query
}
func TestPathRoot(t *testing.T) {
assertPath(t,
"$",
buildPath(
// empty
))
}
func TestPathKey(t *testing.T) {
assertPath(t,
"$.foo",
buildPath(
newMatchKeyFn("foo"),
))
}
func TestPathBracketKey(t *testing.T) {
assertPath(t,
"$[foo]",
buildPath(
newMatchKeyFn("foo"),
))
}
func TestPathBracketStringKey(t *testing.T) {
assertPath(t,
"$['foo']",
buildPath(
newMatchKeyFn("foo"),
))
}
func TestPathIndex(t *testing.T) {
assertPath(t,
"$[123]",
buildPath(
newMatchIndexFn(123),
))
}
func TestPathSliceStart(t *testing.T) {
assertPath(t,
"$[123:]",
buildPath(
newMatchSliceFn().setStart(123),
))
}
func TestPathSliceStartEnd(t *testing.T) {
assertPath(t,
"$[123:456]",
buildPath(
newMatchSliceFn().setStart(123).setEnd(456),
))
}
func TestPathSliceStartEndColon(t *testing.T) {
assertPath(t,
"$[123:456:]",
buildPath(
newMatchSliceFn().setStart(123).setEnd(456),
))
}
func TestPathSliceStartStep(t *testing.T) {
assertPath(t,
"$[123::7]",
buildPath(
newMatchSliceFn().setStart(123).setStep(7),
))
}
func TestPathSliceEndStep(t *testing.T) {
assertPath(t,
"$[:456:7]",
buildPath(
newMatchSliceFn().setEnd(456).setStep(7),
))
}
func TestPathSliceStep(t *testing.T) {
assertPath(t,
"$[::7]",
buildPath(
newMatchSliceFn().setStep(7),
))
}
func TestPathSliceAll(t *testing.T) {
assertPath(t,
"$[123:456:7]",
buildPath(
newMatchSliceFn().setStart(123).setEnd(456).setStep(7),
))
}
func TestPathAny(t *testing.T) {
assertPath(t,
"$.*",
buildPath(
newMatchAnyFn(),
))
}
func TestPathUnion(t *testing.T) {
assertPath(t,
"$[foo, bar, baz]",
buildPath(
&matchUnionFn{[]pathFn{
newMatchKeyFn("foo"),
newMatchKeyFn("bar"),
newMatchKeyFn("baz"),
}},
))
}
func TestPathRecurse(t *testing.T) {
assertPath(t,
"$..*",
buildPath(
newMatchRecursiveFn(),
))
}
func TestPathFilterExpr(t *testing.T) {
assertPath(t,
"$[?('foo'),?(bar)]",
buildPath(
&matchUnionFn{[]pathFn{
newMatchFilterFn("foo", toml.Position{}),
newMatchFilterFn("bar", toml.Position{}),
}},
))
}
+278
View File
@@ -0,0 +1,278 @@
/*
Based on the "jsonpath" spec/concept.
http://goessner.net/articles/JsonPath/
https://code.google.com/p/json-path/
*/
package query
import (
"fmt"
)
const maxInt = int(^uint(0) >> 1)
type queryParser struct {
flow chan token
tokensBuffer []token
query *Query
union []pathFn
err error
}
type queryParserStateFn func() queryParserStateFn
// Formats and panics an error message based on a token
func (p *queryParser) parseError(tok *token, msg string, args ...interface{}) queryParserStateFn {
p.err = fmt.Errorf(tok.Position.String()+": "+msg, args...)
return nil // trigger parse to end
}
func (p *queryParser) run() {
for state := p.parseStart; state != nil; {
state = state()
}
}
func (p *queryParser) backup(tok *token) {
p.tokensBuffer = append(p.tokensBuffer, *tok)
}
func (p *queryParser) peek() *token {
if len(p.tokensBuffer) != 0 {
return &(p.tokensBuffer[0])
}
tok, ok := <-p.flow
if !ok {
return nil
}
p.backup(&tok)
return &tok
}
func (p *queryParser) lookahead(types ...tokenType) bool {
result := true
buffer := []token{}
for _, typ := range types {
tok := p.getToken()
if tok == nil {
result = false
break
}
buffer = append(buffer, *tok)
if tok.typ != typ {
result = false
break
}
}
// add the tokens back to the buffer, and return
p.tokensBuffer = append(p.tokensBuffer, buffer...)
return result
}
func (p *queryParser) getToken() *token {
if len(p.tokensBuffer) != 0 {
tok := p.tokensBuffer[0]
p.tokensBuffer = p.tokensBuffer[1:]
return &tok
}
tok, ok := <-p.flow
if !ok {
return nil
}
return &tok
}
func (p *queryParser) parseStart() queryParserStateFn {
tok := p.getToken()
if tok == nil || tok.typ == tokenEOF {
return nil
}
if tok.typ != tokenDollar {
return p.parseError(tok, "Expected '$' at start of expression")
}
return p.parseMatchExpr
}
// handle '.' prefix, '[]', and '..'
func (p *queryParser) parseMatchExpr() queryParserStateFn {
tok := p.getToken()
switch tok.typ {
case tokenDotDot:
p.query.appendPath(&matchRecursiveFn{})
// nested parse for '..'
tok := p.getToken()
switch tok.typ {
case tokenKey:
p.query.appendPath(newMatchKeyFn(tok.val))
return p.parseMatchExpr
case tokenLeftBracket:
return p.parseBracketExpr
case tokenStar:
// do nothing - the recursive predicate is enough
return p.parseMatchExpr
}
case tokenDot:
// nested parse for '.'
tok := p.getToken()
switch tok.typ {
case tokenKey:
p.query.appendPath(newMatchKeyFn(tok.val))
return p.parseMatchExpr
case tokenStar:
p.query.appendPath(&matchAnyFn{})
return p.parseMatchExpr
}
case tokenLeftBracket:
return p.parseBracketExpr
case tokenEOF:
return nil // allow EOF at this stage
}
return p.parseError(tok, "expected match expression")
}
func (p *queryParser) parseBracketExpr() queryParserStateFn {
if p.lookahead(tokenInteger, tokenColon) {
return p.parseSliceExpr
}
if p.peek().typ == tokenColon {
return p.parseSliceExpr
}
return p.parseUnionExpr
}
func (p *queryParser) parseUnionExpr() queryParserStateFn {
var tok *token
// this state can be traversed after some sub-expressions
// so be careful when setting up state in the parser
if p.union == nil {
p.union = []pathFn{}
}
loop: // labeled loop for easy breaking
for {
if len(p.union) > 0 {
// parse delimiter or terminator
tok = p.getToken()
switch tok.typ {
case tokenComma:
// do nothing
case tokenRightBracket:
break loop
default:
return p.parseError(tok, "expected ',' or ']', not '%s'", tok.val)
}
}
// parse sub expression
tok = p.getToken()
switch tok.typ {
case tokenInteger:
p.union = append(p.union, newMatchIndexFn(tok.Int()))
case tokenKey:
p.union = append(p.union, newMatchKeyFn(tok.val))
case tokenString:
p.union = append(p.union, newMatchKeyFn(tok.val))
case tokenQuestion:
return p.parseFilterExpr
default:
return p.parseError(tok, "expected union sub expression, not '%s', %d", tok.val, len(p.union))
}
}
// if there is only one sub-expression, use that instead
if len(p.union) == 1 {
p.query.appendPath(p.union[0])
} else {
p.query.appendPath(&matchUnionFn{p.union})
}
p.union = nil // clear out state
return p.parseMatchExpr
}
func (p *queryParser) parseSliceExpr() queryParserStateFn {
// init slice to grab all elements
var start, end, step *int = nil, nil, nil
// parse optional start
tok := p.getToken()
if tok.typ == tokenInteger {
v := tok.Int()
start = &v
tok = p.getToken()
}
if tok.typ != tokenColon {
return p.parseError(tok, "expected ':'")
}
// parse optional end
tok = p.getToken()
if tok.typ == tokenInteger {
v := tok.Int()
end = &v
tok = p.getToken()
}
if tok.typ == tokenRightBracket {
p.query.appendPath(&matchSliceFn{Start: start, End: end, Step: step})
return p.parseMatchExpr
}
if tok.typ != tokenColon {
return p.parseError(tok, "expected ']' or ':'")
}
// parse optional step
tok = p.getToken()
if tok.typ == tokenInteger {
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(&matchSliceFn{Start: start, End: end, Step: step})
return p.parseMatchExpr
}
func (p *queryParser) parseFilterExpr() queryParserStateFn {
tok := p.getToken()
if tok.typ != tokenLeftParen {
return p.parseError(tok, "expected left-parenthesis for filter expression")
}
tok = p.getToken()
if tok.typ != tokenKey && tok.typ != tokenString {
return p.parseError(tok, "expected key or string for filter function name")
}
name := tok.val
tok = p.getToken()
if tok.typ != tokenRightParen {
return p.parseError(tok, "expected right-parenthesis for filter expression")
}
p.union = append(p.union, newMatchFilterFn(name, tok.Position))
return p.parseUnionExpr
}
func parseQuery(flow chan token) (*Query, error) {
parser := &queryParser{
flow: flow,
tokensBuffer: []token{},
query: newQuery(),
}
parser.run()
return parser.query, parser.err
}
+613
View File
@@ -0,0 +1,613 @@
package query
import (
"fmt"
"io/ioutil"
"sort"
"strings"
"testing"
"time"
"github.com/pelletier/go-toml"
)
type queryTestNode struct {
value interface{}
position toml.Position
}
func valueString(root interface{}) string {
result := "" //fmt.Sprintf("%T:", root)
switch node := root.(type) {
case *Result:
items := []string{}
for i, v := range node.Values() {
items = append(items, fmt.Sprintf("%s:%s",
node.Positions()[i].String(), valueString(v)))
}
sort.Strings(items)
result = "[" + strings.Join(items, ", ") + "]"
case queryTestNode:
result = fmt.Sprintf("%s:%s",
node.position.String(), valueString(node.value))
case []interface{}:
items := []string{}
for _, v := range node {
items = append(items, valueString(v))
}
sort.Strings(items)
result = "[" + strings.Join(items, ", ") + "]"
case *toml.Tree:
// workaround for unreliable map key ordering
items := []string{}
for _, k := range node.Keys() {
v := node.GetPath([]string{k})
items = append(items, k+":"+valueString(v))
}
sort.Strings(items)
result = "{" + strings.Join(items, ", ") + "}"
case map[string]interface{}:
// workaround for unreliable map key ordering
items := []string{}
for k, v := range node {
items = append(items, k+":"+valueString(v))
}
sort.Strings(items)
result = "{" + strings.Join(items, ", ") + "}"
case int64:
result += fmt.Sprintf("%d", node)
case string:
result += "'" + node + "'"
case float64:
result += fmt.Sprintf("%f", node)
case bool:
result += fmt.Sprintf("%t", node)
case time.Time:
result += fmt.Sprintf("'%v'", node)
}
return result
}
func assertValue(t *testing.T, result, ref interface{}) {
pathStr := valueString(result)
refStr := valueString(ref)
if pathStr != refStr {
t.Errorf("values do not match")
t.Log("test:", pathStr)
t.Log("ref: ", refStr)
}
}
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 {
t.Errorf("Non-nil toml parse error: %v", err)
return
}
q, err := Compile(query)
if err != nil {
t.Error(err)
return
}
results := q.Execute(tree)
assertValue(t, results, ref)
}
func TestQueryRoot(t *testing.T) {
assertQueryPositions(t,
"a = 42",
"$",
[]interface{}{
queryTestNode{
map[string]interface{}{
"a": int64(42),
}, toml.Position{1, 1},
},
})
}
func TestQueryKey(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = 42",
"$.foo.a",
[]interface{}{
queryTestNode{
int64(42), toml.Position{2, 1},
},
})
}
func TestQueryKeyString(t *testing.T) {
assertQueryPositions(t,
"[foo]\na = 42",
"$.foo['a']",
[]interface{}{
queryTestNode{
int64(42), toml.Position{2, 1},
},
})
}
func TestQueryKeyUnicodeString(t *testing.T) {
assertQueryPositions(t,
"['f𝟘.o']\na = 42",
"$['f𝟘.o']['a']",
[]interface{}{
queryTestNode{
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 = [0,1,2,3,4,5,6,7,8,9]",
"$.foo.a[:5]",
[]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}},
queryTestNode{int64(4), toml.Position{2, 1}},
})
}
func TestQuerySliceStep(t *testing.T) {
assertQueryPositions(t,
"[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{
[]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}},
})
}
func TestQueryAny(t *testing.T) {
assertQueryPositions(t,
"[foo.bar]\na=1\nb=2\n[foo.baz]\na=3\nb=4",
"$.foo.*",
[]interface{}{
queryTestNode{
map[string]interface{}{
"a": int64(1),
"b": int64(2),
}, toml.Position{1, 1},
},
queryTestNode{
map[string]interface{}{
"a": int64(3),
"b": int64(4),
}, toml.Position{4, 1},
},
})
}
func TestQueryUnionSimple(t *testing.T) {
assertQueryPositions(t,
"[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6",
"$.*[bar,foo]",
[]interface{}{
queryTestNode{
map[string]interface{}{
"a": int64(1),
"b": int64(2),
}, toml.Position{1, 1},
},
queryTestNode{
map[string]interface{}{
"a": int64(3),
"b": int64(4),
}, toml.Position{4, 1},
},
queryTestNode{
map[string]interface{}{
"a": int64(5),
"b": int64(6),
}, toml.Position{7, 1},
},
})
}
func TestQueryRecursionAll(t *testing.T) {
assertQueryPositions(t,
"[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6",
"$..*",
[]interface{}{
queryTestNode{
map[string]interface{}{
"foo": map[string]interface{}{
"bar": map[string]interface{}{
"a": int64(1),
"b": int64(2),
},
},
"baz": map[string]interface{}{
"foo": map[string]interface{}{
"a": int64(3),
"b": int64(4),
},
},
"gorf": map[string]interface{}{
"foo": map[string]interface{}{
"a": int64(5),
"b": int64(6),
},
},
}, toml.Position{1, 1},
},
queryTestNode{
map[string]interface{}{
"bar": map[string]interface{}{
"a": int64(1),
"b": int64(2),
},
}, toml.Position{1, 1},
},
queryTestNode{
map[string]interface{}{
"a": int64(1),
"b": int64(2),
}, toml.Position{1, 1},
},
queryTestNode{int64(1), toml.Position{2, 1}},
queryTestNode{int64(2), toml.Position{3, 1}},
queryTestNode{
map[string]interface{}{
"foo": map[string]interface{}{
"a": int64(3),
"b": int64(4),
},
}, toml.Position{4, 1},
},
queryTestNode{
map[string]interface{}{
"a": int64(3),
"b": int64(4),
}, toml.Position{4, 1},
},
queryTestNode{int64(3), toml.Position{5, 1}},
queryTestNode{int64(4), toml.Position{6, 1}},
queryTestNode{
map[string]interface{}{
"foo": map[string]interface{}{
"a": int64(5),
"b": int64(6),
},
}, toml.Position{7, 1},
},
queryTestNode{
map[string]interface{}{
"a": int64(5),
"b": int64(6),
}, toml.Position{7, 1},
},
queryTestNode{int64(5), toml.Position{8, 1}},
queryTestNode{int64(6), toml.Position{9, 1}},
})
}
func TestQueryRecursionUnionSimple(t *testing.T) {
assertQueryPositions(t,
"[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6",
"$..['foo','bar']",
[]interface{}{
queryTestNode{
map[string]interface{}{
"bar": map[string]interface{}{
"a": int64(1),
"b": int64(2),
},
}, toml.Position{1, 1},
},
queryTestNode{
map[string]interface{}{
"a": int64(3),
"b": int64(4),
}, toml.Position{4, 1},
},
queryTestNode{
map[string]interface{}{
"a": int64(1),
"b": int64(2),
}, toml.Position{1, 1},
},
queryTestNode{
map[string]interface{}{
"a": int64(5),
"b": int64(6),
}, toml.Position{7, 1},
},
})
}
func TestQueryFilterFn(t *testing.T) {
buff, err := ioutil.ReadFile("../example.toml")
if err != nil {
t.Error(err)
return
}
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}},
})
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}},
})
assertQueryPositions(t, string(buff),
"$..[?(float)]",
[]interface{}{
queryTestNode{4e-08, toml.Position{30, 1}},
})
tv, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z")
assertQueryPositions(t, string(buff),
"$..[?(tree)]",
[]interface{}{
queryTestNode{
map[string]interface{}{
"name": "Tom Preston-Werner",
"organization": "GitHub",
"bio": "GitHub Cofounder & CEO\nLikes tater tots and beer.",
"dob": tv,
}, toml.Position{5, 1},
},
queryTestNode{
map[string]interface{}{
"server": "192.168.1.1",
"ports": []interface{}{int64(8001), int64(8001), int64(8002)},
"connection_max": int64(5000),
"enabled": true,
}, toml.Position{11, 1},
},
queryTestNode{
map[string]interface{}{
"alpha": map[string]interface{}{
"ip": "10.0.0.1",
"dc": "eqdc10",
},
"beta": map[string]interface{}{
"ip": "10.0.0.2",
"dc": "eqdc10",
},
}, toml.Position{17, 1},
},
queryTestNode{
map[string]interface{}{
"ip": "10.0.0.1",
"dc": "eqdc10",
}, toml.Position{20, 3},
},
queryTestNode{
map[string]interface{}{
"ip": "10.0.0.2",
"dc": "eqdc10",
}, toml.Position{24, 3},
},
queryTestNode{
map[string]interface{}{
"data": []interface{}{
[]interface{}{"gamma", "delta"},
[]interface{}{int64(1), int64(2)},
},
"score": 4e-08,
}, toml.Position{28, 1},
},
})
assertQueryPositions(t, string(buff),
"$..[?(time)]",
[]interface{}{
queryTestNode{tv, toml.Position{9, 1}},
})
assertQueryPositions(t, string(buff),
"$..[?(bool)]",
[]interface{}{
queryTestNode{true, toml.Position{15, 1}},
})
}
+158
View File
@@ -0,0 +1,158 @@
package query
import (
"time"
"github.com/pelletier/go-toml"
)
// NodeFilterFn represents a user-defined filter function, for use with
// Query.SetFilter().
//
// The return value of the function must indicate if 'node' is to be included
// at this stage of the TOML path. Returning true will include the node, and
// returning false will exclude it.
//
// NOTE: Care should be taken to write script callbacks such that they are safe
// to use from multiple goroutines.
type NodeFilterFn func(node interface{}) bool
// Result is the result of Executing a Query.
type Result struct {
items []interface{}
positions []toml.Position
}
// appends a value/position pair to the result set.
func (r *Result) appendResult(node interface{}, pos toml.Position) {
r.items = append(r.items, node)
r.positions = append(r.positions, pos)
}
// Values is a set of values within a Result. The order of values is not
// guaranteed to be in document order, and may be different each time a query is
// executed.
func (r Result) Values() []interface{} {
return r.items
}
// Positions is a set of positions for values within a Result. Each index
// in Positions() corresponds to the entry in Value() of the same index.
func (r Result) Positions() []toml.Position {
return r.positions
}
// runtime context for executing query paths
type queryContext struct {
result *Result
filters *map[string]NodeFilterFn
lastPosition toml.Position
}
// generic path functor interface
type pathFn interface {
setNext(next pathFn)
// it is the caller's responsibility to set the ctx.lastPosition before invoking call()
// node can be one of: *toml.Tree, []*toml.Tree, or a scalar
call(node interface{}, ctx *queryContext)
}
// A Query is the representation of a compiled TOML path. A Query is safe
// for concurrent use by multiple goroutines.
type Query struct {
root pathFn
tail pathFn
filters *map[string]NodeFilterFn
}
func newQuery() *Query {
return &Query{
root: nil,
tail: nil,
filters: &defaultFilterFunctions,
}
}
func (q *Query) appendPath(next pathFn) {
if q.root == nil {
q.root = next
} else {
q.tail.setNext(next)
}
q.tail = next
next.setNext(newTerminatingFn()) // init the next functor
}
// Compile compiles a TOML path expression. The returned Query can be used
// to match elements within a Tree and its descendants. See Execute.
func Compile(path string) (*Query, error) {
return parseQuery(lexQuery(path))
}
// Execute executes a query against a Tree, and returns the result of the query.
func (q *Query) Execute(tree *toml.Tree) *Result {
result := &Result{
items: []interface{}{},
positions: []toml.Position{},
}
if q.root == nil {
result.appendResult(tree, tree.GetPosition(""))
} else {
ctx := &queryContext{
result: result,
filters: q.filters,
}
ctx.lastPosition = tree.Position()
q.root.call(tree, ctx)
}
return result
}
// CompileAndExecute is a shorthand for Compile(path) followed by Execute(tree).
func CompileAndExecute(path string, tree *toml.Tree) (*Result, error) {
query, err := Compile(path)
if err != nil {
return nil, err
}
return query.Execute(tree), nil
}
// SetFilter sets a user-defined filter function. These may be used inside
// "?(..)" query expressions to filter TOML document elements within a query.
func (q *Query) SetFilter(name string, fn NodeFilterFn) {
if q.filters == &defaultFilterFunctions {
// clone the static table
q.filters = &map[string]NodeFilterFn{}
for k, v := range defaultFilterFunctions {
(*q.filters)[k] = v
}
}
(*q.filters)[name] = fn
}
var defaultFilterFunctions = map[string]NodeFilterFn{
"tree": func(node interface{}) bool {
_, ok := node.(*toml.Tree)
return ok
},
"int": func(node interface{}) bool {
_, ok := node.(int64)
return ok
},
"float": func(node interface{}) bool {
_, ok := node.(float64)
return ok
},
"string": func(node interface{}) bool {
_, ok := node.(string)
return ok
},
"time": func(node interface{}) bool {
_, ok := node.(time.Time)
return ok
},
"bool": func(node interface{}) bool {
_, ok := node.(bool)
return ok
},
}
+151
View File
@@ -0,0 +1,151 @@
package query
import (
"fmt"
"testing"
"github.com/pelletier/go-toml"
)
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 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]]
title = "The Stand"
author = "Stephen King"
[[book]]
title = "For Whom the Bell Tolls"
author = "Ernest Hemmingway"
[[book]]
title = "Neuromancer"
author = "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) {
config, _ := toml.Load(`
[postgres]
user = "pelletier"
password = "mypassword"
`)
checkQuery(t, config, "$..[user,password]", "pelletier", "mypassword")
}
func TestQueryPathNotPresent(t *testing.T) {
config, _ := toml.Load(`a = "hello"`)
query, err := Compile("$.foo.bar")
if err != nil {
t.Fatal("unexpected error:", err)
}
results := query.Execute(config)
if err != nil {
t.Fatalf("err should be nil. got %s instead", err)
}
if len(results.items) != 0 {
t.Fatalf("no items should be matched. %d matched instead", len(results.items))
}
}
func ExampleNodeFilterFn_filterExample() {
tree, _ := toml.Load(`
[struct_one]
foo = "foo"
bar = "bar"
[struct_two]
baz = "baz"
gorf = "gorf"
`)
// create a query that references a user-defined-filter
query, _ := Compile("$[?(bazOnly)]")
// define the filter, and assign it to the query
query.SetFilter("bazOnly", func(node interface{}) bool {
if tree, ok := node.(*toml.Tree); ok {
return tree.Has("baz")
}
return false // reject all other node types
})
// results contain only the 'struct_two' Tree
query.Execute(tree)
}
func ExampleQuery_queryExample() {
config, _ := toml.Load(`
[[book]]
title = "The Stand"
author = "Stephen King"
[[book]]
title = "For Whom the Bell Tolls"
author = "Ernest Hemmingway"
[[book]]
title = "Neuromancer"
author = "William Gibson"
`)
// find and print all the authors in the document
query, _ := Compile("$.book.author")
authors := query.Execute(config)
for _, name := range authors.Values() {
fmt.Println(name)
}
}
func TestTomlQuery(t *testing.T) {
tree, err := toml.Load("[foo.bar]\na=1\nb=2\n[baz.foo]\na=3\nb=4\n[gorf.foo]\na=5\nb=6")
if err != nil {
t.Error(err)
return
}
query, err := Compile("$.foo.bar")
if err != nil {
t.Error(err)
return
}
result := query.Execute(tree)
values := result.Values()
if len(values) != 1 {
t.Errorf("Expected resultset of 1, got %d instead: %v", len(values), values)
}
if tt, ok := values[0].(*toml.Tree); !ok {
t.Errorf("Expected type of Tree: %T", values[0])
} else if tt.Get("a") != int64(1) {
t.Errorf("Expected 'a' with a value 1: %v", tt.Get("a"))
} else if tt.Get("b") != int64(2) {
t.Errorf("Expected 'b' with a value 2: %v", tt.Get("b"))
}
}
+106
View File
@@ -0,0 +1,106 @@
package query
import (
"fmt"
"strconv"
"github.com/pelletier/go-toml"
)
// Define tokens
type tokenType int
const (
eof = -(iota + 1)
)
const (
tokenError tokenType = iota
tokenEOF
tokenKey
tokenString
tokenInteger
tokenFloat
tokenLeftBracket
tokenRightBracket
tokenLeftParen
tokenRightParen
tokenComma
tokenColon
tokenDollar
tokenStar
tokenQuestion
tokenDot
tokenDotDot
)
var tokenTypeNames = []string{
"Error",
"EOF",
"Key",
"String",
"Integer",
"Float",
"[",
"]",
"(",
")",
",",
":",
"$",
"*",
"?",
".",
"..",
}
type token struct {
toml.Position
typ tokenType
val string
}
func (tt tokenType) String() string {
idx := int(tt)
if idx < len(tokenTypeNames) {
return tokenTypeNames[idx]
}
return "Unknown"
}
func (t token) Int() int {
if result, err := strconv.Atoi(t.val); err != nil {
panic(err)
} else {
return result
}
}
func (t token) String() string {
switch t.typ {
case tokenEOF:
return "EOF"
case tokenError:
return t.val
}
return fmt.Sprintf("%q", t.val)
}
func isSpace(r rune) bool {
return r == ' ' || r == '\t'
}
func isAlphanumeric(r rune) bool {
return 'a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' || r == '_'
}
func isDigit(r rune) bool {
return '0' <= r && r <= '9'
}
func isHexDigit(r rune) bool {
return isDigit(r) ||
(r >= 'a' && r <= 'f') ||
(r >= 'A' && r <= 'F')
}
-168
View File
@@ -1,168 +0,0 @@
package toml
import "fmt"
func scanFollows(pattern []byte) func(b []byte) bool {
return func(b []byte) bool {
if len(b) < len(pattern) {
return false
}
for i, c := range pattern {
if b[i] != c {
return false
}
}
return true
}
}
var scanFollowsMultilineBasicStringDelimiter = scanFollows([]byte{'"', '"', '"'})
var scanFollowsMultilineLiteralStringDelimiter = scanFollows([]byte{'\'', '\'', '\''})
var scanFollowsTrue = scanFollows([]byte{'t', 'r', 'u', 'e'})
var scanFollowsFalse = scanFollows([]byte{'f', 'a', 'l', 's', 'e'})
var scanFollowsInf = scanFollows([]byte{'i', 'n', 'f'})
var scanFollowsNan = scanFollows([]byte{'n', 'a', 'n'})
func scanUnquotedKey(b []byte) ([]byte, []byte, error) {
//unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
for i := 0; i < len(b); i++ {
if !isUnquotedKeyChar(b[i]) {
return b[:i], b[i:], nil
}
}
return b, b[len(b):], nil
}
func isUnquotedKeyChar(r byte) bool {
return (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' || r == '_'
}
func scanLiteralString(b []byte) ([]byte, []byte, error) {
//literal-string = apostrophe *literal-char apostrophe
//apostrophe = %x27 ; ' apostrophe
//literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
for i := 1; i < len(b); i++ {
switch b[i] {
case '\'':
return b[:i+1], b[i+1:], nil
case '\n':
return nil, nil, newDecodeError(b[i:i+1], "literal strings cannot have new lines")
}
}
return nil, nil, newDecodeError(b[len(b):], "unterminated literal string")
}
func scanMultilineLiteralString(b []byte) ([]byte, []byte, error) {
//ml-literal-string = ml-literal-string-delim [ newline ] ml-literal-body
//ml-literal-string-delim
//ml-literal-string-delim = 3apostrophe
//ml-literal-body = *mll-content *( mll-quotes 1*mll-content ) [ mll-quotes ]
//
//mll-content = mll-char / newline
//mll-char = %x09 / %x20-26 / %x28-7E / non-ascii
//mll-quotes = 1*2apostrophe
for i := 3; i < len(b); i++ {
switch b[i] {
case '\'':
if scanFollowsMultilineLiteralStringDelimiter(b[i:]) {
return b[:i+3], b[i+3:], nil
}
}
}
return nil, nil, newDecodeError(b[len(b):], `multiline literal string not terminated by '''`)
}
func scanWindowsNewline(b []byte) ([]byte, []byte, error) {
if len(b) < 2 {
return nil, nil, fmt.Errorf(`windows new line missing \n`)
}
if b[1] != '\n' {
return nil, nil, fmt.Errorf(`windows new line should be \r\n`)
}
return b[:2], b[2:], nil
}
func scanWhitespace(b []byte) ([]byte, []byte) {
for i := 0; i < len(b); i++ {
switch b[i] {
case ' ', '\t':
continue
default:
return b[:i], b[i:]
}
}
return b, b[len(b):]
}
func scanComment(b []byte) ([]byte, []byte, error) {
//;; Comment
//
//comment-start-symbol = %x23 ; #
//non-ascii = %x80-D7FF / %xE000-10FFFF
//non-eol = %x09 / %x20-7F / non-ascii
//
//comment = comment-start-symbol *non-eol
for i := 1; i < len(b); i++ {
switch b[i] {
case '\n':
return b[:i], b[i:], nil
}
}
return b, nil, nil
}
// TODO perform validation on the string?
func scanBasicString(b []byte) ([]byte, []byte, error) {
//basic-string = quotation-mark *basic-char quotation-mark
//quotation-mark = %x22 ; "
//basic-char = basic-unescaped / escaped
//basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
//escaped = escape escape-seq-char
for i := 1; i < len(b); i++ {
switch b[i] {
case '"':
return b[:i+1], b[i+1:], nil
case '\n':
return nil, nil, newDecodeError(b[i:i+1], "basic strings cannot have new lines")
case '\\':
if len(b) < i+2 {
return nil, nil, newDecodeError(b[i:i+1], "need a character after \\")
}
i++ // skip the next character
}
}
return nil, nil, fmt.Errorf(`basic string not terminated by "`)
}
// TODO perform validation on the string?
func scanMultilineBasicString(b []byte) ([]byte, []byte, error) {
//ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body
//ml-basic-string-delim
//ml-basic-string-delim = 3quotation-mark
//ml-basic-body = *mlb-content *( mlb-quotes 1*mlb-content ) [ mlb-quotes ]
//
//mlb-content = mlb-char / newline / mlb-escaped-nl
//mlb-char = mlb-unescaped / escaped
//mlb-quotes = 1*2quotation-mark
//mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
//mlb-escaped-nl = escape ws newline *( wschar / newline )
for i := 3; i < len(b); i++ {
switch b[i] {
case '"':
if scanFollowsMultilineBasicStringDelimiter(b[i:]) {
return b[:i+3], b[i+3:], nil
}
case '\\':
if len(b) < i+2 {
return nil, nil, newDecodeError(b[len(b):], "need a character after \\")
}
i++ // skip the next character
}
}
return nil, nil, newDecodeError(b[len(b):], `multiline basic string not terminated by """`)
}
-554
View File
@@ -1,554 +0,0 @@
package toml
import (
"fmt"
"math"
"reflect"
"strings"
"sync"
)
type target interface {
// Dereferences the target.
get() reflect.Value
// Store a string at the target.
setString(v string) error
// Store a boolean at the target
setBool(v bool) error
// Store an int64 at the target
setInt64(v int64) error
// Store a float64 at the target
setFloat64(v float64) error
// Stores any value at the target
set(v reflect.Value) error
}
// valueTarget just contains a reflect.Value that can be set.
// It is used for struct fields.
type valueTarget reflect.Value
func (t valueTarget) get() reflect.Value {
return reflect.Value(t)
}
func (t valueTarget) set(v reflect.Value) error {
reflect.Value(t).Set(v)
return nil
}
func (t valueTarget) setString(v string) error {
t.get().SetString(v)
return nil
}
func (t valueTarget) setBool(v bool) error {
t.get().SetBool(v)
return nil
}
func (t valueTarget) setInt64(v int64) error {
t.get().SetInt(v)
return nil
}
func (t valueTarget) setFloat64(v float64) error {
t.get().SetFloat(v)
return nil
}
// interfaceTarget wraps an other target to dereference on get.
type interfaceTarget struct {
x target
}
func (t interfaceTarget) get() reflect.Value {
return t.x.get().Elem()
}
func (t interfaceTarget) set(v reflect.Value) error {
return t.x.set(v)
}
func (t interfaceTarget) setString(v string) error {
return t.x.setString(v)
}
func (t interfaceTarget) setBool(v bool) error {
return t.x.setBool(v)
}
func (t interfaceTarget) setInt64(v int64) error {
return t.x.setInt64(v)
}
func (t interfaceTarget) setFloat64(v float64) error {
return t.x.setFloat64(v)
}
// mapTarget targets a specific key of a map.
type mapTarget struct {
v reflect.Value
k reflect.Value
}
func (t mapTarget) get() reflect.Value {
return t.v.MapIndex(t.k)
}
func (t mapTarget) set(v reflect.Value) error {
t.v.SetMapIndex(t.k, v)
return nil
}
func (t mapTarget) setString(v string) error {
return t.set(reflect.ValueOf(v))
}
func (t mapTarget) setBool(v bool) error {
return t.set(reflect.ValueOf(v))
}
func (t mapTarget) setInt64(v int64) error {
return t.set(reflect.ValueOf(v))
}
func (t mapTarget) setFloat64(v float64) error {
return t.set(reflect.ValueOf(v))
}
// makes sure that the value pointed at by t is indexable (Slice, Array), or
// dereferences to an indexable (Ptr, Interface).
func ensureValueIndexable(t target) error {
f := t.get()
switch f.Type().Kind() {
case reflect.Slice:
if f.IsNil() {
return t.set(reflect.MakeSlice(f.Type(), 0, 0))
}
case reflect.Interface:
if f.IsNil() || f.Elem().Type() != sliceInterfaceType {
return t.set(reflect.MakeSlice(sliceInterfaceType, 0, 0))
}
if f.Elem().Type().Kind() != reflect.Slice {
return fmt.Errorf("interface is pointing to a %s, not a slice", f.Kind())
}
case reflect.Ptr:
if f.IsNil() {
ptr := reflect.New(f.Type().Elem())
err := t.set(ptr)
if err != nil {
return err
}
f = t.get()
}
return ensureValueIndexable(valueTarget(f.Elem()))
case reflect.Array:
// arrays are always initialized.
default:
return fmt.Errorf("cannot initialize a slice in %s", f.Kind())
}
return nil
}
var sliceInterfaceType = reflect.TypeOf([]interface{}{})
var mapStringInterfaceType = reflect.TypeOf(map[string]interface{}{})
func ensureMapIfInterface(x target) {
v := x.get()
if v.Kind() == reflect.Interface && v.IsNil() {
newElement := reflect.MakeMap(mapStringInterfaceType)
x.set(newElement)
}
}
func setString(t target, v string) error {
f := t.get()
switch f.Kind() {
case reflect.String:
return t.setString(v)
case reflect.Interface:
return t.set(reflect.ValueOf(v))
default:
return fmt.Errorf("cannot assign string to a %s", f.Kind())
}
}
func setBool(t target, v bool) error {
f := t.get()
switch f.Kind() {
case reflect.Bool:
return t.setBool(v)
case reflect.Interface:
return t.set(reflect.ValueOf(v))
default:
return fmt.Errorf("cannot assign bool to a %s", f.String())
}
}
const maxInt = int64(^uint(0) >> 1)
const minInt = -maxInt - 1
func setInt64(t target, v int64) error {
f := t.get()
switch f.Kind() {
case reflect.Int64:
return t.setInt64(v)
case reflect.Int32:
if v < math.MinInt32 || v > math.MaxInt32 {
return fmt.Errorf("integer %d does not fit in an int32", v)
}
return t.set(reflect.ValueOf(int32(v)))
case reflect.Int16:
if v < math.MinInt16 || v > math.MaxInt16 {
return fmt.Errorf("integer %d does not fit in an int16", v)
}
return t.set(reflect.ValueOf(int16(v)))
case reflect.Int8:
if v < math.MinInt8 || v > math.MaxInt8 {
return fmt.Errorf("integer %d does not fit in an int8", v)
}
return t.set(reflect.ValueOf(int8(v)))
case reflect.Int:
if v < minInt || v > maxInt {
return fmt.Errorf("integer %d does not fit in an int", v)
}
return t.set(reflect.ValueOf(int(v)))
case reflect.Uint64:
if v < 0 {
return fmt.Errorf("negative integer %d cannot be stored in an uint64", v)
}
return t.set(reflect.ValueOf(uint64(v)))
case reflect.Uint32:
if v < 0 {
return fmt.Errorf("negative integer %d cannot be stored in an uint32", v)
}
if v > math.MaxUint32 {
return fmt.Errorf("integer %d cannot be stored in an uint32", v)
}
return t.set(reflect.ValueOf(uint32(v)))
case reflect.Uint16:
if v < 0 {
return fmt.Errorf("negative integer %d cannot be stored in an uint16", v)
}
if v > math.MaxUint16 {
return fmt.Errorf("integer %d cannot be stored in an uint16", v)
}
return t.set(reflect.ValueOf(uint16(v)))
case reflect.Uint8:
if v < 0 {
return fmt.Errorf("negative integer %d cannot be stored in an uint8", v)
}
if v > math.MaxUint8 {
return fmt.Errorf("integer %d cannot be stored in an uint8", v)
}
return t.set(reflect.ValueOf(uint8(v)))
case reflect.Uint:
if v < 0 {
return fmt.Errorf("negative integer %d cannot be stored in an uint", v)
}
return t.set(reflect.ValueOf(uint(v)))
case reflect.Interface:
return t.set(reflect.ValueOf(v))
default:
return fmt.Errorf("cannot assign int64 to a %s", f.String())
}
}
func setFloat64(t target, v float64) error {
f := t.get()
switch f.Kind() {
case reflect.Float64:
return t.setFloat64(v)
case reflect.Float32:
if v > math.MaxFloat32 {
return fmt.Errorf("float %f cannot be stored in a float32", v)
}
return t.set(reflect.ValueOf(float32(v)))
case reflect.Interface:
return t.set(reflect.ValueOf(v))
default:
return fmt.Errorf("cannot assign float64 to a %s", f.String())
}
}
// Returns the element at idx of the value pointed at by target, or an error if
// t does not point to an indexable.
// If the target points to an Array and idx is out of bounds, it returns
// (nil, nil) as this is not a fatal error (the unmarshaler will skip).
func elementAt(t target, idx int) (target, error) {
f := t.get()
switch f.Kind() {
case reflect.Slice:
// TODO: use the idx function argument and avoid alloc if possible.
idx := f.Len()
err := t.set(reflect.Append(f, reflect.New(f.Type().Elem()).Elem()))
if err != nil {
return nil, err
}
return valueTarget(t.get().Index(idx)), nil
case reflect.Array:
if idx >= f.Len() {
return nil, nil
}
return valueTarget(f.Index(idx)), nil
case reflect.Interface:
if f.IsNil() {
panic("interface should have been initialized")
}
ifaceElem := f.Elem()
if ifaceElem.Kind() != reflect.Slice {
return nil, fmt.Errorf("cannot elementAt on a %s", f.Kind())
}
idx := ifaceElem.Len()
newElem := reflect.New(ifaceElem.Type().Elem()).Elem()
newSlice := reflect.Append(ifaceElem, newElem)
err := t.set(newSlice)
if err != nil {
return nil, err
}
return valueTarget(t.get().Elem().Index(idx)), nil
case reflect.Ptr:
return elementAt(valueTarget(f.Elem()), idx)
default:
return nil, fmt.Errorf("cannot elementAt on a %s", f.Kind())
}
}
func (d *decoder) scopeTableTarget(append bool, t target, name string) (target, bool, error) {
x := t.get()
switch x.Kind() {
// Kinds that need to recurse
case reflect.Interface:
t, err := scopeInterface(append, t)
if err != nil {
return t, false, err
}
return d.scopeTableTarget(append, t, name)
case reflect.Ptr:
t, err := scopePtr(t)
if err != nil {
return t, false, err
}
return d.scopeTableTarget(append, t, name)
case reflect.Slice:
t, err := scopeSlice(append, t)
if err != nil {
return t, false, err
}
append = false
return d.scopeTableTarget(append, t, name)
case reflect.Array:
t, err := d.scopeArray(append, t)
if err != nil {
return t, false, err
}
append = false
return d.scopeTableTarget(append, t, name)
// Terminal kinds
case reflect.Struct:
return scopeStruct(x, name)
case reflect.Map:
if x.IsNil() {
t.set(reflect.MakeMap(x.Type()))
x = t.get()
}
return scopeMap(x, name)
default:
panic(fmt.Errorf("can't scope on a %s", x.Kind()))
}
}
func scopeInterface(append bool, t target) (target, error) {
err := initInterface(append, t)
if err != nil {
return t, err
}
return interfaceTarget{t}, nil
}
func scopePtr(t target) (target, error) {
err := initPtr(t)
if err != nil {
return t, err
}
return valueTarget(t.get().Elem()), nil
}
func initPtr(t target) error {
x := t.get()
if !x.IsNil() {
return nil
}
return t.set(reflect.New(x.Type().Elem()))
}
// initInterface makes sure that the interface pointed at by the target is not
// nil.
// Returns the target to the initialized value of the target.
func initInterface(append bool, t target) error {
x := t.get()
if x.Kind() != reflect.Interface {
panic("this should only be called on interfaces")
}
if !x.IsNil() && (x.Elem().Type() == sliceInterfaceType || x.Elem().Type() == mapStringInterfaceType) {
return nil
}
var newElement reflect.Value
if append {
newElement = reflect.MakeSlice(sliceInterfaceType, 0, 0)
} else {
newElement = reflect.MakeMap(mapStringInterfaceType)
}
err := t.set(newElement)
if err != nil {
return err
}
return nil
}
func scopeSlice(append bool, t target) (target, error) {
v := t.get()
if append {
newElem := reflect.New(v.Type().Elem())
newSlice := reflect.Append(v, newElem.Elem())
err := t.set(newSlice)
if err != nil {
return t, err
}
v = t.get()
}
return valueTarget(v.Index(v.Len() - 1)), nil
}
func (d *decoder) scopeArray(append bool, t target) (target, error) {
v := t.get()
idx := d.arrayIndex(append, v)
if idx >= v.Len() {
return nil, fmt.Errorf("not enough space in the array")
}
return valueTarget(v.Index(idx)), nil
}
func scopeMap(v reflect.Value, name string) (target, bool, error) {
k := reflect.ValueOf(name)
keyType := v.Type().Key()
if !k.Type().AssignableTo(keyType) {
if !k.Type().ConvertibleTo(keyType) {
return nil, false, fmt.Errorf("cannot convert string into map key type %s", keyType)
}
k = k.Convert(keyType)
}
if !v.MapIndex(k).IsValid() {
newElem := reflect.New(v.Type().Elem())
v.SetMapIndex(k, newElem.Elem())
}
return mapTarget{
v: v,
k: k,
}, true, nil
}
type fieldPathsMap = map[string][]int
type fieldPathsCache struct {
m map[reflect.Type]fieldPathsMap
l sync.RWMutex
}
func (c *fieldPathsCache) get(t reflect.Type) (fieldPathsMap, bool) {
c.l.RLock()
paths, ok := c.m[t]
c.l.RUnlock()
return paths, ok
}
func (c *fieldPathsCache) set(t reflect.Type, m fieldPathsMap) {
c.l.Lock()
c.m[t] = m
c.l.Unlock()
}
var globalFieldPathsCache = fieldPathsCache{
m: map[reflect.Type]fieldPathsMap{},
l: sync.RWMutex{},
}
func scopeStruct(v reflect.Value, name string) (target, bool, error) {
// TODO: cache this, and reduce allocations
fieldPaths, ok := globalFieldPathsCache.get(v.Type())
if !ok {
fieldPaths = map[string][]int{}
path := make([]int, 0, 16)
var walk func(reflect.Value)
walk = func(v reflect.Value) {
t := v.Type()
for i := 0; i < t.NumField(); i++ {
l := len(path)
path = append(path, i)
f := t.Field(i)
if f.PkgPath != "" {
// only consider exported fields
} else if f.Anonymous {
walk(v.Field(i))
} else {
fieldName, ok := f.Tag.Lookup("toml")
if !ok {
fieldName = f.Name
}
pathCopy := make([]int, len(path))
copy(pathCopy, path)
fieldPaths[fieldName] = pathCopy
// extra copy for the case-insensitive match
fieldPaths[strings.ToLower(fieldName)] = pathCopy
}
path = path[:l]
}
}
walk(v)
globalFieldPathsCache.set(v.Type(), fieldPaths)
}
path, ok := fieldPaths[name]
if !ok {
path, ok = fieldPaths[strings.ToLower(name)]
}
if !ok {
return nil, false, nil
}
return valueTarget(v.FieldByIndex(path)), true, nil
}
-184
View File
@@ -1,184 +0,0 @@
package toml
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStructTarget_Ensure(t *testing.T) {
examples := []struct {
desc string
input reflect.Value
name string
test func(v reflect.Value, err error)
}{
{
desc: "handle a nil slice of string",
input: reflect.ValueOf(&struct{ A []string }{}).Elem(),
name: "A",
test: func(v reflect.Value, err error) {
assert.NoError(t, err)
assert.False(t, v.IsNil())
},
},
{
desc: "handle an existing slice of string",
input: reflect.ValueOf(&struct{ A []string }{A: []string{"foo"}}).Elem(),
name: "A",
test: func(v reflect.Value, err error) {
assert.NoError(t, err)
require.False(t, v.IsNil())
s := v.Interface().([]string)
assert.Equal(t, []string{"foo"}, s)
},
},
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
d := decoder{}
target, _, err := d.scopeTableTarget(false, valueTarget(e.input), e.name)
require.NoError(t, err)
err = ensureValueIndexable(target)
v := target.get()
e.test(v, err)
})
}
}
func TestStructTarget_SetString(t *testing.T) {
str := "value"
examples := []struct {
desc string
input reflect.Value
name string
test func(v reflect.Value, err error)
}{
{
desc: "sets a string",
input: reflect.ValueOf(&struct{ A string }{}).Elem(),
name: "A",
test: func(v reflect.Value, err error) {
assert.NoError(t, err)
assert.Equal(t, str, v.String())
},
},
{
desc: "fails on a float",
input: reflect.ValueOf(&struct{ A float64 }{}).Elem(),
name: "A",
test: func(v reflect.Value, err error) {
assert.Error(t, err)
},
},
{
desc: "fails on a slice",
input: reflect.ValueOf(&struct{ A []string }{}).Elem(),
name: "A",
test: func(v reflect.Value, err error) {
assert.Error(t, err)
},
},
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
d := decoder{}
target, _, err := d.scopeTableTarget(false, valueTarget(e.input), e.name)
require.NoError(t, err)
err = setString(target, str)
v := target.get()
e.test(v, err)
})
}
}
func TestPushNew(t *testing.T) {
t.Run("slice of strings", func(t *testing.T) {
type Doc struct {
A []string
}
d := Doc{}
dec := decoder{}
x, _, err := dec.scopeTableTarget(false, valueTarget(reflect.ValueOf(&d).Elem()), "A")
require.NoError(t, err)
n, err := elementAt(x, 0)
require.NoError(t, err)
require.NoError(t, n.setString("hello"))
require.Equal(t, []string{"hello"}, d.A)
n, err = elementAt(x, 1)
require.NoError(t, err)
require.NoError(t, n.setString("world"))
require.Equal(t, []string{"hello", "world"}, d.A)
})
t.Run("slice of interfaces", func(t *testing.T) {
type Doc struct {
A []interface{}
}
d := Doc{}
dec := decoder{}
x, _, err := dec.scopeTableTarget(false, valueTarget(reflect.ValueOf(&d).Elem()), "A")
require.NoError(t, err)
n, err := elementAt(x, 0)
require.NoError(t, err)
require.NoError(t, setString(n, "hello"))
require.Equal(t, []interface{}{"hello"}, d.A)
n, err = elementAt(x, 1)
require.NoError(t, err)
require.NoError(t, setString(n, "world"))
require.Equal(t, []interface{}{"hello", "world"}, d.A)
})
}
func TestScope_Struct(t *testing.T) {
examples := []struct {
desc string
input reflect.Value
name string
err bool
found bool
idx []int
}{
{
desc: "simple field",
input: reflect.ValueOf(&struct{ A string }{}).Elem(),
name: "A",
idx: []int{0},
found: true,
},
{
desc: "fails not-exported field",
input: reflect.ValueOf(&struct{ a string }{}).Elem(),
name: "a",
err: false,
found: false,
},
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
dec := decoder{}
x, found, err := dec.scopeTableTarget(false, valueTarget(e.input), e.name)
assert.Equal(t, e.found, found)
if e.err {
assert.Error(t, err)
}
if found {
x2, ok := x.(valueTarget)
require.True(t, ok)
x2.get()
}
})
}
}
+136
View File
@@ -0,0 +1,136 @@
package toml
import "fmt"
// Define tokens
type tokenType int
const (
eof = -(iota + 1)
)
const (
tokenError tokenType = iota
tokenEOF
tokenComment
tokenKey
tokenString
tokenInteger
tokenTrue
tokenFalse
tokenFloat
tokenInf
tokenNan
tokenEqual
tokenLeftBracket
tokenRightBracket
tokenLeftCurlyBrace
tokenRightCurlyBrace
tokenLeftParen
tokenRightParen
tokenDoubleLeftBracket
tokenDoubleRightBracket
tokenLocalDate
tokenLocalTime
tokenTimeOffset
tokenKeyGroup
tokenKeyGroupArray
tokenComma
tokenColon
tokenDollar
tokenStar
tokenQuestion
tokenDot
tokenDotDot
tokenEOL
)
var tokenTypeNames = []string{
"Error",
"EOF",
"Comment",
"Key",
"String",
"Integer",
"True",
"False",
"Float",
"Inf",
"NaN",
"=",
"[",
"]",
"{",
"}",
"(",
")",
"]]",
"[[",
"LocalDate",
"LocalTime",
"TimeOffset",
"KeyGroup",
"KeyGroupArray",
",",
":",
"$",
"*",
"?",
".",
"..",
"EOL",
}
type token struct {
Position
typ tokenType
val string
}
func (tt tokenType) String() string {
idx := int(tt)
if idx < len(tokenTypeNames) {
return tokenTypeNames[idx]
}
return "Unknown"
}
func (t token) String() string {
switch t.typ {
case tokenEOF:
return "EOF"
case tokenError:
return t.val
}
return fmt.Sprintf("%q", t.val)
}
func isSpace(r rune) bool {
return r == ' ' || r == '\t'
}
func isAlphanumeric(r rune) bool {
return 'a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' || r == '_'
}
func isKeyChar(r rune) bool {
// Keys start with the first character that isn't whitespace or [ and end
// with the last non-whitespace character before the equals sign. Keys
// cannot contain a # character."
return !(r == '\r' || r == '\n' || r == eof || r == '=')
}
func isKeyStartChar(r rune) bool {
return !(isSpace(r) || r == '\r' || r == '\n' || r == eof || r == '[')
}
func isDigit(r rune) bool {
return '0' <= r && r <= '9'
}
func isHexDigit(r rune) bool {
return isDigit(r) ||
(r >= 'a' && r <= 'f') ||
(r >= 'A' && r <= 'F')
}
+69
View File
@@ -0,0 +1,69 @@
package toml
import "testing"
func TestTokenStringer(t *testing.T) {
var tests = []struct {
tt tokenType
expect string
}{
{tokenError, "Error"},
{tokenEOF, "EOF"},
{tokenComment, "Comment"},
{tokenKey, "Key"},
{tokenString, "String"},
{tokenInteger, "Integer"},
{tokenTrue, "True"},
{tokenFalse, "False"},
{tokenFloat, "Float"},
{tokenEqual, "="},
{tokenLeftBracket, "["},
{tokenRightBracket, "]"},
{tokenLeftCurlyBrace, "{"},
{tokenRightCurlyBrace, "}"},
{tokenLeftParen, "("},
{tokenRightParen, ")"},
{tokenDoubleLeftBracket, "]]"},
{tokenDoubleRightBracket, "[["},
{tokenLocalDate, "LocalDate"},
{tokenLocalTime, "LocalTime"},
{tokenTimeOffset, "TimeOffset"},
{tokenKeyGroup, "KeyGroup"},
{tokenKeyGroupArray, "KeyGroupArray"},
{tokenComma, ","},
{tokenColon, ":"},
{tokenDollar, "$"},
{tokenStar, "*"},
{tokenQuestion, "?"},
{tokenDot, "."},
{tokenDotDot, ".."},
{tokenEOL, "EOL"},
{tokenEOL + 1, "Unknown"},
}
for i, test := range tests {
got := test.tt.String()
if got != test.expect {
t.Errorf("[%d] invalid string of token type; got %q, expected %q", i, got, test.expect)
}
}
}
func TestTokenString(t *testing.T) {
var tests = []struct {
tok token
expect string
}{
{token{Position{1, 1}, tokenEOF, ""}, "EOF"},
{token{Position{1, 1}, tokenError, "Δt"}, "Δt"},
{token{Position{1, 1}, tokenString, "bar"}, `"bar"`},
{token{Position{1, 1}, tokenString, "123456789012345"}, `"123456789012345"`},
}
for i, test := range tests {
got := test.tok.String()
if got != test.expect {
t.Errorf("[%d] invalid of string token; got %q, expected %q", i, got, test.expect)
}
}
}
-242
View File
@@ -1,242 +0,0 @@
;; This document describes TOML's syntax, using the ABNF format (defined in
;; RFC 5234 -- https://www.ietf.org/rfc/rfc5234.txt).
;;
;; All valid TOML documents will match this description, however certain
;; invalid documents would need to be rejected as per the semantics described
;; in the supporting text description.
;; It is possible to try this grammar interactively, using instaparse.
;; http://instaparse.mojombo.com/
;;
;; To do so, in the lower right, click on Options and change `:input-format` to
;; ':abnf'. Then paste this entire ABNF document into the grammar entry box
;; (above the options). Then you can type or paste a sample TOML document into
;; the beige box on the left. Tada!
;; Overall Structure
toml = expression *( newline expression )
expression = ws [ comment ]
expression =/ ws keyval ws [ comment ]
expression =/ ws table ws [ comment ]
;; Whitespace
ws = *wschar
wschar = %x20 ; Space
wschar =/ %x09 ; Horizontal tab
;; Newline
newline = %x0A ; LF
newline =/ %x0D.0A ; CRLF
;; Comment
comment-start-symbol = %x23 ; #
non-ascii = %x80-D7FF / %xE000-10FFFF
non-eol = %x09 / %x20-7F / non-ascii
comment = comment-start-symbol *non-eol
;; Key-Value pairs
keyval = key keyval-sep val
key = simple-key / dotted-key
simple-key = quoted-key / unquoted-key
unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _
quoted-key = basic-string / literal-string
dotted-key = simple-key 1*( dot-sep simple-key )
dot-sep = ws %x2E ws ; . Period
keyval-sep = ws %x3D ws ; =
val = string / boolean / array / inline-table / date-time / float / integer
;; String
string = ml-basic-string / basic-string / ml-literal-string / literal-string
;; Basic String
basic-string = quotation-mark *basic-char quotation-mark
quotation-mark = %x22 ; "
basic-char = basic-unescaped / escaped
basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
escaped = escape escape-seq-char
escape = %x5C ; \
escape-seq-char = %x22 ; " quotation mark U+0022
escape-seq-char =/ %x5C ; \ reverse solidus U+005C
escape-seq-char =/ %x62 ; b backspace U+0008
escape-seq-char =/ %x66 ; f form feed U+000C
escape-seq-char =/ %x6E ; n line feed U+000A
escape-seq-char =/ %x72 ; r carriage return U+000D
escape-seq-char =/ %x74 ; t tab U+0009
escape-seq-char =/ %x75 4HEXDIG ; uXXXX U+XXXX
escape-seq-char =/ %x55 8HEXDIG ; UXXXXXXXX U+XXXXXXXX
;; Multiline Basic String
ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body
ml-basic-string-delim
ml-basic-string-delim = 3quotation-mark
ml-basic-body = *mlb-content *( mlb-quotes 1*mlb-content ) [ mlb-quotes ]
mlb-content = mlb-char / newline / mlb-escaped-nl
mlb-char = mlb-unescaped / escaped
mlb-quotes = 1*2quotation-mark
mlb-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii
mlb-escaped-nl = escape ws newline *( wschar / newline )
;; Literal String
literal-string = apostrophe *literal-char apostrophe
apostrophe = %x27 ; ' apostrophe
literal-char = %x09 / %x20-26 / %x28-7E / non-ascii
;; Multiline Literal String
ml-literal-string = ml-literal-string-delim [ newline ] ml-literal-body
ml-literal-string-delim
ml-literal-string-delim = 3apostrophe
ml-literal-body = *mll-content *( mll-quotes 1*mll-content ) [ mll-quotes ]
mll-content = mll-char / newline
mll-char = %x09 / %x20-26 / %x28-7E / non-ascii
mll-quotes = 1*2apostrophe
;; Integer
integer = dec-int / hex-int / oct-int / bin-int
minus = %x2D ; -
plus = %x2B ; +
underscore = %x5F ; _
digit1-9 = %x31-39 ; 1-9
digit0-7 = %x30-37 ; 0-7
digit0-1 = %x30-31 ; 0-1
hex-prefix = %x30.78 ; 0x
oct-prefix = %x30.6F ; 0o
bin-prefix = %x30.62 ; 0b
dec-int = [ minus / plus ] unsigned-dec-int
unsigned-dec-int = DIGIT / digit1-9 1*( DIGIT / underscore DIGIT )
hex-int = hex-prefix HEXDIG *( HEXDIG / underscore HEXDIG )
oct-int = oct-prefix digit0-7 *( digit0-7 / underscore digit0-7 )
bin-int = bin-prefix digit0-1 *( digit0-1 / underscore digit0-1 )
;; Float
float = float-int-part ( exp / frac [ exp ] )
float =/ special-float
float-int-part = dec-int
frac = decimal-point zero-prefixable-int
decimal-point = %x2E ; .
zero-prefixable-int = DIGIT *( DIGIT / underscore DIGIT )
exp = "e" float-exp-part
float-exp-part = [ minus / plus ] zero-prefixable-int
special-float = [ minus / plus ] ( inf / nan )
inf = %x69.6e.66 ; inf
nan = %x6e.61.6e ; nan
;; Boolean
boolean = true / false
true = %x74.72.75.65 ; true
false = %x66.61.6C.73.65 ; false
;; Date and Time (as defined in RFC 3339)
date-time = offset-date-time / local-date-time / local-date / local-time
date-fullyear = 4DIGIT
date-month = 2DIGIT ; 01-12
date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year
time-delim = "T" / %x20 ; T, t, or space
time-hour = 2DIGIT ; 00-23
time-minute = 2DIGIT ; 00-59
time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second rules
time-secfrac = "." 1*DIGIT
time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
time-offset = "Z" / time-numoffset
partial-time = time-hour ":" time-minute ":" time-second [ time-secfrac ]
full-date = date-fullyear "-" date-month "-" date-mday
full-time = partial-time time-offset
;; Offset Date-Time
offset-date-time = full-date time-delim full-time
;; Local Date-Time
local-date-time = full-date time-delim partial-time
;; Local Date
local-date = full-date
;; Local Time
local-time = partial-time
;; Array
array = array-open [ array-values ] ws-comment-newline array-close
array-open = %x5B ; [
array-close = %x5D ; ]
array-values = ws-comment-newline val ws-comment-newline array-sep array-values
array-values =/ ws-comment-newline val ws-comment-newline [ array-sep ]
array-sep = %x2C ; , Comma
ws-comment-newline = *( wschar / [ comment ] newline )
;; Table
table = std-table / array-table
;; Standard Table
std-table = std-table-open key std-table-close
std-table-open = %x5B ws ; [ Left square bracket
std-table-close = ws %x5D ; ] Right square bracket
;; Inline Table
inline-table = inline-table-open [ inline-table-keyvals ] inline-table-close
inline-table-open = %x7B ws ; {
inline-table-close = ws %x7D ; }
inline-table-sep = ws %x2C ws ; , Comma
inline-table-keyvals = keyval [ inline-table-sep inline-table-keyvals ]
;; Array Table
array-table = array-table-open key array-table-close
array-table-open = %x5B.5B ws ; [[ Double left square bracket
array-table-close = ws %x5D.5D ; ]] Double right square bracket
;; Built-in ABNF terms, reproduced here for clarity
ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
DIGIT = %x30-39 ; 0-9
HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
+533
View File
@@ -0,0 +1,533 @@
package toml
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"runtime"
"strings"
)
type tomlValue struct {
value interface{} // string, int64, uint64, float64, bool, time.Time, [] of any of this list
comment string
commented bool
multiline bool
literal bool
position Position
}
// Tree is the result of the parsing of a TOML file.
type Tree struct {
values map[string]interface{} // string -> *tomlValue, *Tree, []*Tree
comment string
commented bool
inline bool
position Position
}
func newTree() *Tree {
return newTreeWithPosition(Position{})
}
func newTreeWithPosition(pos Position) *Tree {
return &Tree{
values: make(map[string]interface{}),
position: pos,
}
}
// TreeFromMap initializes a new Tree object using the given map.
func TreeFromMap(m map[string]interface{}) (*Tree, error) {
result, err := toTree(m)
if err != nil {
return nil, err
}
return result.(*Tree), nil
}
// Position returns the position of the tree.
func (t *Tree) Position() Position {
return t.position
}
// Has returns a boolean indicating if the given key exists.
func (t *Tree) Has(key string) bool {
if key == "" {
return false
}
return t.HasPath(strings.Split(key, "."))
}
// HasPath returns true if the given path of keys exists, false otherwise.
func (t *Tree) HasPath(keys []string) bool {
return t.GetPath(keys) != nil
}
// Keys returns the keys of the toplevel tree (does not recurse).
func (t *Tree) Keys() []string {
keys := make([]string, len(t.values))
i := 0
for k := range t.values {
keys[i] = k
i++
}
return keys
}
// Get the value at key in the Tree.
// Key is a dot-separated path (e.g. a.b.c) without single/double quoted strings.
// If you need to retrieve non-bare keys, use GetPath.
// 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) Get(key string) interface{} {
if key == "" {
return t
}
return t.GetPath(strings.Split(key, "."))
}
// GetPath returns the element in the tree indicated by 'keys'.
// If keys is of length zero, the current tree is returned.
func (t *Tree) GetPath(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:
return node.value
default:
return node
}
}
// 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 == "" {
return t.position
}
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 {
if len(keys) == 0 {
return t.position
}
subtree := t
for _, intermediateKey := range keys[:len(keys)-1] {
value, exists := subtree.values[intermediateKey]
if !exists {
return Position{0, 0}
}
switch node := value.(type) {
case *Tree:
subtree = node
case []*Tree:
// go to most recent element
if len(node) == 0 {
return Position{0, 0}
}
subtree = node[len(node)-1]
default:
return Position{0, 0}
}
}
// branch based on final node type
switch node := subtree.values[keys[len(keys)-1]].(type) {
case *tomlValue:
return node.position
case *Tree:
return node.position
case []*Tree:
// go to most recent element
if len(node) == 0 {
return Position{0, 0}
}
return node[len(node)-1].position
default:
return Position{0, 0}
}
}
// GetDefault works like Get but with a default value
func (t *Tree) GetDefault(key string, def interface{}) interface{} {
val := t.Get(key)
if val == nil {
return def
}
return val
}
// SetOptions arguments are supplied to the SetWithOptions and SetPathWithOptions functions to modify marshalling behaviour.
// The default values within the struct are valid default options.
type SetOptions struct {
Comment string
Commented bool
Multiline bool
Literal bool
}
// SetWithOptions is the same as Set, but allows you to provide formatting
// instructions to the key, that will be used by Marshal().
func (t *Tree) SetWithOptions(key string, opts SetOptions, value interface{}) {
t.SetPathWithOptions(strings.Split(key, "."), opts, value)
}
// SetPathWithOptions is the same as SetPath, but allows you to provide
// formatting instructions to the key, that will be reused by Marshal().
func (t *Tree) SetPathWithOptions(keys []string, opts SetOptions, value interface{}) {
subtree := t
for i, intermediateKey := range keys[:len(keys)-1] {
nextTree, exists := subtree.values[intermediateKey]
if !exists {
nextTree = newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col})
subtree.values[intermediateKey] = nextTree // add new element here
}
switch node := nextTree.(type) {
case *Tree:
subtree = node
case []*Tree:
// go to most recent element
if len(node) == 0 {
// create element if it does not exist
node = append(node, newTreeWithPosition(Position{Line: t.position.Line + i, Col: t.position.Col}))
subtree.values[intermediateKey] = node
}
subtree = node[len(node)-1]
}
}
var toInsert interface{}
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
v.literal = opts.Literal
toInsert = v
default:
toInsert = &tomlValue{value: value,
comment: opts.Comment,
commented: opts.Commented,
multiline: opts.Multiline,
literal: opts.Literal,
position: Position{Line: subtree.position.Line + len(subtree.values) + 1, Col: subtree.position.Col}}
}
subtree.values[keys[len(keys)-1]] = toInsert
}
// Set an element in the tree.
// Key is a dot-separated path (e.g. a.b.c).
// Creates all necessary intermediate trees, if needed.
func (t *Tree) Set(key string, value interface{}) {
t.SetWithComment(key, "", false, value)
}
// SetWithComment is the same as Set, but allows you to provide comment
// information to the key, that will be reused by Marshal().
func (t *Tree) SetWithComment(key string, comment string, commented bool, value interface{}) {
t.SetPathWithComment(strings.Split(key, "."), comment, commented, value)
}
// SetPath sets an element in the tree.
// Keys is an array of path elements (e.g. {"a","b","c"}).
// Creates all necessary intermediate trees, if needed.
func (t *Tree) SetPath(keys []string, value interface{}) {
t.SetPathWithComment(keys, "", false, value)
}
// SetPathWithComment is the same as SetPath, but allows you to provide comment
// information to the key, that will be reused by Marshal().
func (t *Tree) SetPathWithComment(keys []string, comment string, commented bool, value interface{}) {
t.SetPathWithOptions(keys, SetOptions{Comment: comment, Commented: commented}, value)
}
// Delete removes a key from the tree.
// Key is a dot-separated path (e.g. a.b.c).
func (t *Tree) Delete(key string) error {
keys, err := parseKey(key)
if err != nil {
return err
}
return t.DeletePath(keys)
}
// DeletePath removes a key from the tree.
// Keys is an array of path elements (e.g. {"a","b","c"}).
func (t *Tree) DeletePath(keys []string) error {
keyLen := len(keys)
if keyLen == 1 {
delete(t.values, keys[0])
return nil
}
tree := t.GetPath(keys[:keyLen-1])
item := keys[keyLen-1]
switch node := tree.(type) {
case *Tree:
delete(node.values, item)
return nil
}
return errors.New("no such key to delete")
}
// createSubTree takes a tree and a key and create the necessary intermediate
// subtrees to create a subtree at that point. In-place.
//
// e.g. passing a.b.c will create (assuming tree is empty) tree[a], tree[a][b]
// and tree[a][b][c]
//
// Returns nil on success, error object on failure
func (t *Tree) createSubTree(keys []string, pos Position) error {
subtree := t
for i, intermediateKey := range keys {
nextTree, exists := subtree.values[intermediateKey]
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
}
switch node := nextTree.(type) {
case []*Tree:
subtree = node[len(node)-1]
case *Tree:
subtree = node
default:
return fmt.Errorf("unknown type for path %s (%s): %T (%#v)",
strings.Join(keys, "."), intermediateKey, nextTree, nextTree)
}
}
return nil
}
// LoadBytes creates a Tree from a []byte.
func LoadBytes(b []byte) (tree *Tree, err error) {
defer func() {
if r := recover(); r != nil {
if _, ok := r.(runtime.Error); ok {
panic(r)
}
err = fmt.Errorf("%s", r)
}
}()
if len(b) >= 4 && (hasUTF32BigEndianBOM4(b) || hasUTF32LittleEndianBOM4(b)) {
b = b[4:]
} else if len(b) >= 3 && hasUTF8BOM3(b) {
b = b[3:]
} else if len(b) >= 2 && (hasUTF16BigEndianBOM2(b) || hasUTF16LittleEndianBOM2(b)) {
b = b[2:]
}
tree = parseToml(lexToml(b))
return
}
func hasUTF16BigEndianBOM2(b []byte) bool {
return b[0] == 0xFE && b[1] == 0xFF
}
func hasUTF16LittleEndianBOM2(b []byte) bool {
return b[0] == 0xFF && b[1] == 0xFE
}
func hasUTF8BOM3(b []byte) bool {
return b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF
}
func hasUTF32BigEndianBOM4(b []byte) bool {
return b[0] == 0x00 && b[1] == 0x00 && b[2] == 0xFE && b[3] == 0xFF
}
func hasUTF32LittleEndianBOM4(b []byte) bool {
return b[0] == 0xFF && b[1] == 0xFE && b[2] == 0x00 && b[3] == 0x00
}
// LoadReader creates a Tree from any io.Reader.
func LoadReader(reader io.Reader) (tree *Tree, err error) {
inputBytes, err := ioutil.ReadAll(reader)
if err != nil {
return
}
tree, err = LoadBytes(inputBytes)
return
}
// Load creates a Tree from a string.
func Load(content string) (tree *Tree, err error) {
return LoadBytes([]byte(content))
}
// LoadFile creates a Tree from a file.
func LoadFile(path string) (tree *Tree, err error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
return LoadReader(file)
}
+261
View File
@@ -0,0 +1,261 @@
// Testing support for go-toml
package toml
import (
"reflect"
"testing"
)
func TestTomlHas(t *testing.T) {
tree, _ := Load(`
[test]
key = "value"
`)
if !tree.Has("test.key") {
t.Errorf("Has - expected test.key to exists")
}
if tree.Has("") {
t.Errorf("Should return false if the key is not provided")
}
}
func TestTomlGet(t *testing.T) {
tree, _ := Load(`
[test]
key = "value"
`)
if tree.Get("") != tree {
t.Errorf("Get should return the tree itself when given an empty path")
}
if tree.Get("test.key") != "value" {
t.Errorf("Get should return the value")
}
if tree.Get(`\`) != nil {
t.Errorf("should return nil when the key is malformed")
}
}
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]
key = "value"
`)
if tree.GetDefault("", "hello") != tree {
t.Error("GetDefault should return the tree itself when given an empty path")
}
if tree.GetDefault("test.key", "hello") != "value" {
t.Error("Get should return the value")
}
if tree.GetDefault("whatever", "hello") != "hello" {
t.Error("GetDefault should return the default value if the key does not exist")
}
}
func TestTomlHasPath(t *testing.T) {
tree, _ := Load(`
[test]
key = "value"
`)
if !tree.HasPath([]string{"test", "key"}) {
t.Errorf("HasPath - expected test.key to exists")
}
}
func TestTomlDelete(t *testing.T) {
tree, _ := Load(`
key = "value"
`)
err := tree.Delete("key")
if err != nil {
t.Errorf("Delete - unexpected error while deleting key: %s", err.Error())
}
if tree.Get("key") != nil {
t.Errorf("Delete should have removed key but did not.")
}
}
func TestTomlDeleteUnparsableKey(t *testing.T) {
tree, _ := Load(`
key = "value"
`)
err := tree.Delete(".")
if err == nil {
t.Errorf("Delete should error")
}
}
func TestTomlDeleteNestedKey(t *testing.T) {
tree, _ := Load(`
[foo]
[foo.bar]
key = "value"
`)
err := tree.Delete("foo.bar.key")
if err != nil {
t.Errorf("Error while deleting nested key: %s", err.Error())
}
if tree.Get("key") != nil {
t.Errorf("Delete should have removed nested key but did not.")
}
}
func TestTomlDeleteNonexistentNestedKey(t *testing.T) {
tree, _ := Load(`
[foo]
[foo.bar]
key = "value"
`)
err := tree.Delete("foo.not.there.key")
if err == nil {
t.Errorf("Delete should have thrown an error trying to delete key in nonexistent tree")
}
}
func TestTomlGetPath(t *testing.T) {
node := newTree()
//TODO: set other node data
for idx, item := range []struct {
Path []string
Expected *Tree
}{
{ // empty path test
[]string{},
node,
},
} {
result := node.GetPath(item.Path)
if result != item.Expected {
t.Errorf("GetPath[%d] %v - expected %v, got %v instead.", idx, item.Path, item.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.GetPath([]string{"whatever"}) != nil {
t.Error("GetPath should return nil when the key does not exist")
}
}
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)
if err != nil {
t.Fatal("unexpected error:", err)
}
if tree.Get("hello") != int64(42) {
t.Fatal("hello should be 42, not", tree.Get("hello"))
}
}
func TestLoadBytesBOM(t *testing.T) {
payloads := [][]byte{
[]byte("\xFE\xFFhello=1"),
[]byte("\xFF\xFEhello=1"),
[]byte("\xEF\xBB\xBFhello=1"),
[]byte("\x00\x00\xFE\xFFhello=1"),
[]byte("\xFF\xFE\x00\x00hello=1"),
}
for _, data := range payloads {
tree, err := LoadBytes(data)
if err != nil {
t.Fatal("unexpected error:", err, "for:", data)
}
v := tree.Get("hello")
if v != int64(1) {
t.Fatal("hello should be 1, not", v)
}
}
}
+86 -107
View File
@@ -1,138 +1,117 @@
// This is a support file for toml_testgen_test.go
package toml_test
package toml
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"reflect"
"testing"
"time"
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/require"
)
func testgenInvalid(t *testing.T, input string) {
t.Helper()
t.Logf("Input TOML:\n%s", input)
doc := map[string]interface{}{}
err := toml.Unmarshal([]byte(input), &doc)
if err == nil {
t.Log(json.Marshal(doc))
t.Fatalf("test did not fail")
tree, err := Load(input)
if err != nil {
return
}
typedTree := testgenTranslate(*tree)
buf := new(bytes.Buffer)
if err := json.NewEncoder(buf).Encode(typedTree); err != nil {
return
}
t.Fatalf("test did not fail. resulting tree:\n%s", buf.String())
}
func testgenValid(t *testing.T, input string, jsonRef string) {
t.Helper()
t.Logf("Input TOML:\n%s", input)
doc := map[string]interface{}{}
err := toml.Unmarshal([]byte(input), &doc)
tree, err := Load(input)
if err != nil {
t.Fatalf("failed parsing toml: %s", err)
}
refDoc := testgenBuildRefDoc(jsonRef)
typedTree := testgenTranslate(*tree)
require.Equal(t, refDoc, doc)
out, err := toml.Marshal(doc)
require.NoError(t, err)
doc2 := map[string]interface{}{}
err = toml.Unmarshal(out, &doc2)
require.NoError(t, err)
require.Equal(t, refDoc, doc2)
}
type testGenDescNode struct {
Type string
Value interface{}
}
func testgenBuildRefDoc(jsonRef string) map[string]interface{} {
descTree := map[string]interface{}{}
err := json.Unmarshal([]byte(jsonRef), &descTree)
if err != nil {
panic(fmt.Errorf("reference doc should be valid JSON: %s", err))
buf := new(bytes.Buffer)
if err := json.NewEncoder(buf).Encode(typedTree); err != nil {
t.Fatalf("failed translating to JSON: %s", err)
}
doc := testGenTranslateDesc(descTree)
if doc == nil {
return map[string]interface{}{}
var jsonTest interface{}
if err := json.NewDecoder(buf).Decode(&jsonTest); err != nil {
t.Logf("translated JSON:\n%s", buf.String())
t.Fatalf("failed decoding translated JSON: %s", err)
}
var jsonExpected interface{}
if err := json.NewDecoder(bytes.NewBufferString(jsonRef)).Decode(&jsonExpected); err != nil {
t.Logf("reference JSON:\n%s", jsonRef)
t.Fatalf("failed decoding reference JSON: %s", err)
}
if !reflect.DeepEqual(jsonExpected, jsonTest) {
t.Logf("Diff:\n%#+v\n%#+v", jsonExpected, jsonTest)
t.Fatal("parsed TOML tree is different than expected structure")
}
return doc.(map[string]interface{})
}
func testGenTranslateDesc(input interface{}) interface{} {
a, ok := input.([]interface{})
if ok {
xs := make([]interface{}, len(a))
for i, v := range a {
xs[i] = testGenTranslateDesc(v)
func testgenTranslate(tomlData interface{}) interface{} {
switch orig := tomlData.(type) {
case map[string]interface{}:
typed := make(map[string]interface{}, len(orig))
for k, v := range orig {
typed[k] = testgenTranslate(v)
}
return xs
}
d := input.(map[string]interface{})
var dtype string
var dvalue interface{}
if len(d) == 2 {
dtypeiface, ok := d["type"]
if ok {
dvalue, ok = d["value"]
if ok {
dtype = dtypeiface.(string)
switch dtype {
case "string":
return dvalue.(string)
case "float":
v, err := strconv.ParseFloat(dvalue.(string), 64)
if err != nil {
panic(fmt.Errorf("invalid float '%s': %s", dvalue, err))
}
return v
case "integer":
v, err := strconv.ParseInt(dvalue.(string), 10, 64)
if err != nil {
panic(fmt.Errorf("invalid int '%s': %s", dvalue, err))
}
return v
case "bool":
return dvalue.(string) == "true"
case "datetime":
dt, err := time.Parse("2006-01-02T15:04:05Z", dvalue.(string))
if err != nil {
panic(fmt.Errorf("invalid datetime '%s': %s", dvalue, err))
}
return dt
case "array":
if dvalue == nil {
return nil
}
a := dvalue.([]interface{})
xs := make([]interface{}, len(a))
for i, v := range a {
xs[i] = testGenTranslateDesc(v)
}
return xs
}
panic(fmt.Errorf("unknown type: %s", dtype))
}
return typed
case *Tree:
return testgenTranslate(*orig)
case Tree:
keys := orig.Keys()
typed := make(map[string]interface{}, len(keys))
for _, k := range keys {
typed[k] = testgenTranslate(orig.GetPath([]string{k}))
}
return typed
case []*Tree:
typed := make([]map[string]interface{}, len(orig))
for i, v := range orig {
typed[i] = testgenTranslate(v).(map[string]interface{})
}
return typed
case []map[string]interface{}:
typed := make([]map[string]interface{}, len(orig))
for i, v := range orig {
typed[i] = testgenTranslate(v).(map[string]interface{})
}
return typed
case []interface{}:
typed := make([]interface{}, len(orig))
for i, v := range orig {
typed[i] = testgenTranslate(v)
}
return testgenTag("array", typed)
case time.Time:
return testgenTag("datetime", orig.Format("2006-01-02T15:04:05Z"))
case bool:
return testgenTag("bool", fmt.Sprintf("%v", orig))
case int64:
return testgenTag("integer", fmt.Sprintf("%d", orig))
case float64:
return testgenTag("float", fmt.Sprintf("%v", orig))
case string:
return testgenTag("string", orig)
}
dest := map[string]interface{}{}
for k, v := range d {
dest[k] = testGenTranslateDesc(v)
}
return dest
panic(fmt.Sprintf("Unknown type: %T", tomlData))
}
func testgenTag(typeName string, data interface{}) map[string]interface{} {
return map[string]interface{}{
"type": typeName,
"value": data,
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
// Generated by tomltestgen for toml-test ref 39e37e6 on 2019-03-19T23:58:45-07:00
package toml_test
package toml
import (
"testing"
+71
View File
@@ -0,0 +1,71 @@
package toml
// PubTOMLValue wrapping tomlValue in order to access all properties from outside.
type PubTOMLValue = tomlValue
func (ptv *PubTOMLValue) Value() interface{} {
return ptv.value
}
func (ptv *PubTOMLValue) Comment() string {
return ptv.comment
}
func (ptv *PubTOMLValue) Commented() bool {
return ptv.commented
}
func (ptv *PubTOMLValue) Multiline() bool {
return ptv.multiline
}
func (ptv *PubTOMLValue) Position() Position {
return ptv.position
}
func (ptv *PubTOMLValue) SetValue(v interface{}) {
ptv.value = v
}
func (ptv *PubTOMLValue) SetComment(s string) {
ptv.comment = s
}
func (ptv *PubTOMLValue) SetCommented(c bool) {
ptv.commented = c
}
func (ptv *PubTOMLValue) SetMultiline(m bool) {
ptv.multiline = m
}
func (ptv *PubTOMLValue) SetPosition(p Position) {
ptv.position = p
}
// PubTree wrapping Tree in order to access all properties from outside.
type PubTree = Tree
func (pt *PubTree) Values() map[string]interface{} {
return pt.values
}
func (pt *PubTree) Comment() string {
return pt.comment
}
func (pt *PubTree) Commented() bool {
return pt.commented
}
func (pt *PubTree) Inline() bool {
return pt.inline
}
func (pt *PubTree) SetValues(v map[string]interface{}) {
pt.values = v
}
func (pt *PubTree) SetComment(c string) {
pt.comment = c
}
func (pt *PubTree) SetCommented(c bool) {
pt.commented = c
}
func (pt *PubTree) SetInline(i bool) {
pt.inline = i
}
+155
View File
@@ -0,0 +1,155 @@
package toml
import (
"fmt"
"reflect"
"time"
)
var kindToType = [reflect.String + 1]reflect.Type{
reflect.Bool: reflect.TypeOf(true),
reflect.String: reflect.TypeOf(""),
reflect.Float32: reflect.TypeOf(float64(1)),
reflect.Float64: reflect.TypeOf(float64(1)),
reflect.Int: reflect.TypeOf(int64(1)),
reflect.Int8: reflect.TypeOf(int64(1)),
reflect.Int16: reflect.TypeOf(int64(1)),
reflect.Int32: reflect.TypeOf(int64(1)),
reflect.Int64: reflect.TypeOf(int64(1)),
reflect.Uint: reflect.TypeOf(uint64(1)),
reflect.Uint8: reflect.TypeOf(uint64(1)),
reflect.Uint16: reflect.TypeOf(uint64(1)),
reflect.Uint32: reflect.TypeOf(uint64(1)),
reflect.Uint64: reflect.TypeOf(uint64(1)),
}
// typeFor returns a reflect.Type for a reflect.Kind, or nil if none is found.
// supported values:
// string, bool, int64, uint64, float64, time.Time, int, int8, int16, int32, uint, uint8, uint16, uint32, float32
func typeFor(k reflect.Kind) reflect.Type {
if k > 0 && int(k) < len(kindToType) {
return kindToType[k]
}
return nil
}
func simpleValueCoercion(object interface{}) (interface{}, error) {
switch original := object.(type) {
case string, bool, int64, uint64, float64, time.Time:
return original, nil
case int:
return int64(original), nil
case int8:
return int64(original), nil
case int16:
return int64(original), nil
case int32:
return int64(original), nil
case uint:
return uint64(original), nil
case uint8:
return uint64(original), nil
case uint16:
return uint64(original), nil
case uint32:
return uint64(original), nil
case float32:
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)
}
}
func sliceToTree(object interface{}) (interface{}, error) {
// arrays are a bit tricky, since they can represent either a
// collection of simple values, which is represented by one
// *tomlValue, or an array of tables, which is represented by an
// array of *Tree.
// holding the assumption that this function is called from toTree only when value.Kind() is Array or Slice
value := reflect.ValueOf(object)
insideType := value.Type().Elem()
length := value.Len()
if length > 0 {
insideType = reflect.ValueOf(value.Index(0).Interface()).Type()
}
if insideType.Kind() == reflect.Map {
// this is considered as an array of tables
tablesArray := make([]*Tree, 0, length)
for i := 0; i < length; i++ {
table := value.Index(i)
tree, err := toTree(table.Interface())
if err != nil {
return nil, err
}
tablesArray = append(tablesArray, tree.(*Tree))
}
return tablesArray, nil
}
sliceType := typeFor(insideType.Kind())
if sliceType == nil {
sliceType = insideType
}
arrayValue := reflect.MakeSlice(reflect.SliceOf(sliceType), 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 &tomlValue{value: arrayValue.Interface(), position: Position{}}, nil
}
func toTree(object interface{}) (interface{}, error) {
value := reflect.ValueOf(object)
if value.Kind() == reflect.Map {
values := map[string]interface{}{}
keys := value.MapKeys()
for _, key := range keys {
if key.Kind() != reflect.String {
if _, ok := key.Interface().(string); !ok {
return nil, fmt.Errorf("map key needs to be a string, not %T (%v)", key.Interface(), key.Kind())
}
}
v := value.MapIndex(key)
newValue, err := toTree(v.Interface())
if err != nil {
return nil, err
}
values[key.String()] = newValue
}
return &Tree{values: values, position: Position{}}, nil
}
if value.Kind() == reflect.Array || value.Kind() == reflect.Slice {
return sliceToTree(object)
}
simpleValue, err := simpleValueCoercion(object)
if err != nil {
return nil, err
}
return &tomlValue{value: simpleValue, position: Position{}}, nil
}
+243
View File
@@ -0,0 +1,243 @@
package toml
import (
"reflect"
"strconv"
"testing"
"time"
)
type customString string
type stringer struct{}
func (s stringer) String() string {
return "stringer"
}
func validate(t *testing.T, path string, object interface{}) {
switch o := object.(type) {
case *Tree:
for key, tree := range o.values {
validate(t, path+"."+key, tree)
}
case []*Tree:
for index, tree := range o {
validate(t, path+"."+strconv.Itoa(index), tree)
}
case *tomlValue:
switch o.value.(type) {
case int64, uint64, bool, string, float64, time.Time,
[]int64, []uint64, []bool, []string, []float64, []time.Time:
default:
t.Fatalf("tomlValue at key %s containing incorrect type %T", path, o.value)
}
default:
t.Fatalf("value at key %s is of incorrect type %T", path, object)
}
t.Logf("validation ok %s as %T", path, object)
}
func validateTree(t *testing.T, tree *Tree) {
validate(t, "", tree)
}
func TestTreeCreateToTree(t *testing.T) {
data := map[string]interface{}{
"a_string": "bar",
"an_int": 42,
"time": time.Now(),
"int8": int8(2),
"int16": int16(2),
"int32": int32(2),
"uint8": uint8(2),
"uint16": uint16(2),
"uint32": uint32(2),
"float32": float32(2),
"a_bool": false,
"stringer": stringer{},
"nested": map[string]interface{}{
"foo": "bar",
},
"array": []string{"a", "b", "c"},
"array_uint": []uint{uint(1), uint(2)},
"array_table": []map[string]interface{}{{"sub_map": 52}},
"array_times": []time.Time{time.Now(), time.Now()},
"map_times": map[string]time.Time{"now": time.Now()},
"custom_string_map_key": map[customString]interface{}{customString("custom"): "custom"},
}
tree, err := TreeFromMap(data)
if err != nil {
t.Fatal("unexpected error:", err)
}
validateTree(t, tree)
}
func TestTreeCreateToTreeInvalidLeafType(t *testing.T) {
_, err := TreeFromMap(map[string]interface{}{"foo": t})
expected := "cannot convert type *testing.T to Tree"
if err.Error() != expected {
t.Fatalf("expected error %s, got %s", expected, err.Error())
}
}
func TestTreeCreateToTreeInvalidMapKeyType(t *testing.T) {
_, err := TreeFromMap(map[string]interface{}{"foo": map[int]interface{}{2: 1}})
expected := "map key needs to be a string, not int (int)"
if err.Error() != expected {
t.Fatalf("expected error %s, got %s", expected, err.Error())
}
}
func TestTreeCreateToTreeInvalidArrayMemberType(t *testing.T) {
_, err := TreeFromMap(map[string]interface{}{"foo": []*testing.T{t}})
expected := "cannot convert type *testing.T to Tree"
if err.Error() != expected {
t.Fatalf("expected error %s, got %s", expected, err.Error())
}
}
func TestTreeCreateToTreeInvalidTableGroupType(t *testing.T) {
_, err := TreeFromMap(map[string]interface{}{"foo": []map[string]interface{}{{"hello": t}}})
expected := "cannot convert type *testing.T to Tree"
if err.Error() != expected {
t.Fatalf("expected error %s, got %s", expected, err.Error())
}
}
func TestRoundTripArrayOfTables(t *testing.T) {
orig := "\n[[stuff]]\n name = \"foo\"\n things = [\"a\", \"b\"]\n"
tree, err := Load(orig)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
m := tree.ToMap()
tree, err = TreeFromMap(m)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
want := orig
got := tree.String()
if got != want {
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)
}
}
+552
View File
@@ -0,0 +1,552 @@
package toml
import (
"bytes"
"fmt"
"io"
"math"
"math/big"
"reflect"
"sort"
"strconv"
"strings"
"time"
)
type valueComplexity int
const (
valueSimple valueComplexity = iota + 1
valueComplex
)
type sortNode struct {
key string
complexity valueComplexity
}
// 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, commented string) string {
var b bytes.Buffer
adjacentQuoteCount := 0
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" + commented)
case '\f':
b.WriteString(`\f`)
case '\r':
b.WriteString("\r")
case '"':
if adjacentQuoteCount >= 3 || i == len(value)-1 {
adjacentQuoteCount = 0
b.WriteString(`\"`)
} else {
b.WriteString(`"`)
}
case '\\':
b.WriteString(`\`)
default:
intRr := uint16(rr)
if intRr < 0x001F {
b.WriteString(fmt.Sprintf("\\u%0.4X", intRr))
} else {
b.WriteRune(rr)
}
}
}
return b.String()
}
// Encodes a string to a TOML-compliant string value
func encodeTomlString(value string) string {
var b bytes.Buffer
for _, rr := range value {
switch rr {
case '\b':
b.WriteString(`\b`)
case '\t':
b.WriteString(`\t`)
case '\n':
b.WriteString(`\n`)
case '\f':
b.WriteString(`\f`)
case '\r':
b.WriteString(`\r`)
case '"':
b.WriteString(`\"`)
case '\\':
b.WriteString(`\\`)
default:
intRr := uint16(rr)
if intRr < 0x001F {
b.WriteString(fmt.Sprintf("\\u%0.4X", intRr))
} else {
b.WriteRune(rr)
}
}
}
return b.String()
}
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)
if ok {
v = tv.value
} else {
tv = &tomlValue{}
}
switch value := v.(type) {
case uint64:
return strconv.FormatUint(value, 10), nil
case int64:
return strconv.FormatInt(value, 10), nil
case float64:
// Default bit length is full 64
bits := 64
// Float panics if nan is used
if !math.IsNaN(value) {
// if 32 bit accuracy is enough to exactly show, use 32
_, acc := big.NewFloat(value).Float32()
if acc == big.Exact {
bits = 32
}
}
if math.Trunc(value) == value {
return strings.ToLower(strconv.FormatFloat(value, 'f', 1, bits)), nil
}
return strings.ToLower(strconv.FormatFloat(value, 'f', -1, bits)), nil
case string:
if tv.multiline {
if tv.literal {
b := strings.Builder{}
b.WriteString("'''\n")
b.Write([]byte(value))
b.WriteString("\n'''")
return b.String(), nil
} else {
return "\"\"\"\n" + encodeMultilineTomlString(value, commented) + "\"\"\"", nil
}
}
return "\"" + encodeTomlString(value) + "\"", nil
case []byte:
b, _ := v.([]byte)
return string(b), nil
case bool:
if value {
return "true", nil
}
return "false", nil
case time.Time:
return value.Format(time.RFC3339), nil
case LocalDate:
return value.String(), nil
case LocalDateTime:
return value.String(), nil
case LocalTime:
return value.String(), nil
case *Tree:
return tomlTreeStringRepresentation(value, ord)
case nil:
return "", nil
}
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Slice {
var values []string
for i := 0; i < rv.Len(); i++ {
item := rv.Index(i).Interface()
itemRepr, err := tomlValueStringRepresentation(item, commented, indent, ord, arraysOneElementPerLine)
if err != nil {
return "", err
}
values = append(values, itemRepr)
}
if arraysOneElementPerLine && len(values) > 1 {
stringBuffer := bytes.Buffer{}
valueIndent := indent + ` ` // TODO: move that to a shared encoder state
stringBuffer.WriteString("[\n")
for _, value := range values {
stringBuffer.WriteString(valueIndent)
stringBuffer.WriteString(commented + value)
stringBuffer.WriteString(`,`)
stringBuffer.WriteString("\n")
}
stringBuffer.WriteString(indent + commented + "]")
return stringBuffer.String(), nil
}
return "[" + strings.Join(values, ", ") + "]", nil
}
return "", fmt.Errorf("unsupported value type %T: %v", v, v)
}
func getTreeArrayLine(trees []*Tree) (line int) {
// Prevent returning 0 for empty trees
line = int(^uint(0) >> 1)
// get lowest line number >= 0
for _, tv := range trees {
if tv.position.Line < line || line == 0 {
line = tv.position.Line
}
}
return
}
func sortByLines(t *Tree) (vals []sortNode) {
var (
line int
lines []int
tv *Tree
tom *tomlValue
node sortNode
)
vals = make([]sortNode, 0)
m := make(map[int]sortNode)
for k := range t.values {
v := t.values[k]
switch v.(type) {
case *Tree:
tv = v.(*Tree)
line = tv.position.Line
node = sortNode{key: k, complexity: valueComplex}
case []*Tree:
line = getTreeArrayLine(v.([]*Tree))
node = sortNode{key: k, complexity: valueComplex}
default:
tom = v.(*tomlValue)
line = tom.position.Line
node = sortNode{key: k, complexity: valueSimple}
}
lines = append(lines, line)
vals = append(vals, node)
m[line] = node
}
sort.Ints(lines)
for i, line := range lines {
vals[i] = m[line]
}
return vals
}
func sortAlphabetical(t *Tree) (vals []sortNode) {
var (
node sortNode
simpVals []string
compVals []string
)
vals = make([]sortNode, 0)
m := make(map[string]sortNode)
for k := range t.values {
v := t.values[k]
switch v.(type) {
case *Tree, []*Tree:
node = sortNode{key: k, complexity: valueComplex}
compVals = append(compVals, node.key)
default:
node = sortNode{key: k, complexity: valueSimple}
simpVals = append(simpVals, node.key)
}
vals = append(vals, node)
m[node.key] = node
}
// Simples first to match previous implementation
sort.Strings(simpVals)
i := 0
for _, key := range simpVals {
vals[i] = m[key]
i++
}
sort.Strings(compVals)
for _, key := range compVals {
vals[i] = m[key]
i++
}
return vals
}
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, " ", false, false)
}
func (t *Tree) writeToOrdered(w io.Writer, indent, keyspace string, bytesCount int64, arraysOneElementPerLine bool, ord MarshalOrder, indentString string, compactComments, parentCommented bool) (int64, error) {
var orderedVals []sortNode
switch ord {
case OrderPreserve:
orderedVals = sortByLines(t)
default:
orderedVals = sortAlphabetical(t)
}
for _, node := range orderedVals {
switch node.complexity {
case valueComplex:
k := node.key
v := t.values[k]
combinedKey := quoteKeyIfNeeded(k)
if keyspace != "" {
combinedKey = keyspace + "." + combinedKey
}
switch node := v.(type) {
// node has to be of those two types given how keys are sorted above
case *Tree:
tv, ok := t.values[k].(*Tree)
if !ok {
return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k])
}
if tv.comment != "" {
comment := strings.Replace(tv.comment, "\n", "\n"+indent+"#", -1)
start := "# "
if strings.HasPrefix(comment, "#") {
start = ""
}
writtenBytesCountComment, errc := writeStrings(w, "\n", indent, start, comment)
bytesCount += int64(writtenBytesCountComment)
if errc != nil {
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+indentString, combinedKey, bytesCount, arraysOneElementPerLine, ord, indentString, compactComments, 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+indentString, combinedKey, bytesCount, arraysOneElementPerLine, ord, indentString, compactComments, parentCommented || t.commented || subTree.commented)
if err != nil {
return bytesCount, err
}
}
}
default: // Simple
k := node.key
v, ok := t.values[k].(*tomlValue)
if !ok {
return bytesCount, fmt.Errorf("invalid value type at %s: %T", k, t.values[k])
}
var commented string
if parentCommented || t.commented || v.commented {
commented = "# "
}
repr, err := tomlValueStringRepresentation(v, commented, indent, ord, arraysOneElementPerLine)
if err != nil {
return bytesCount, err
}
if v.comment != "" {
comment := strings.Replace(v.comment, "\n", "\n"+indent+"#", -1)
start := "# "
if strings.HasPrefix(comment, "#") {
start = ""
}
if !compactComments {
writtenBytesCountComment, errc := writeStrings(w, "\n")
bytesCount += int64(writtenBytesCountComment)
if errc != nil {
return bytesCount, errc
}
}
writtenBytesCountComment, errc := writeStrings(w, indent, start, comment, "\n")
bytesCount += int64(writtenBytesCountComment)
if errc != nil {
return bytesCount, errc
}
}
quotedKey := quoteKeyIfNeeded(k)
writtenBytesCount, err := writeStrings(w, indent, commented, quotedKey, " = ", repr, "\n")
bytesCount += int64(writtenBytesCount)
if err != nil {
return bytesCount, err
}
}
}
return bytesCount, nil
}
// quote a key if it does not fit the bare key format (A-Za-z0-9_-)
// quoted keys use the same rules as strings
func quoteKeyIfNeeded(k string) string {
// when encoding a map with the 'quoteMapKeys' option enabled, the tree will contain
// keys that have already been quoted.
// not an ideal situation, but good enough of a stop gap.
if len(k) >= 2 && k[0] == '"' && k[len(k)-1] == '"' {
return k
}
isBare := true
for _, r := range k {
if !isValidBareChar(r) {
isBare = false
break
}
}
if isBare {
return k
}
return quoteKey(k)
}
func quoteKey(k string) string {
return "\"" + encodeTomlString(k) + "\""
}
func writeStrings(w io.Writer, s ...string) (int, error) {
var n int
for i := range s {
b, err := io.WriteString(w, s[i])
n += b
if err != nil {
return n, err
}
}
return n, nil
}
// WriteTo encode the Tree as Toml and writes it to the writer w.
// Returns the number of bytes written in case of success, or an error if anything happened.
func (t *Tree) WriteTo(w io.Writer) (int64, error) {
return t.writeTo(w, "", "", 0, false)
}
// ToTomlString generates a human-readable representation of the current tree.
// Output spans multiple lines, and is suitable for ingest by a TOML parser.
// If the conversion cannot be performed, ToString returns a non-nil error.
func (t *Tree) ToTomlString() (string, error) {
b, err := t.Marshal()
if err != nil {
return "", err
}
return string(b), nil
}
// String generates a human-readable representation of the current tree.
// Alias of ToString. Present to implement the fmt.Stringer interface.
func (t *Tree) String() string {
result, _ := t.ToTomlString()
return result
}
// ToMap recursively generates a representation of the tree using Go built-in structures.
// The following types are used:
//
// * bool
// * float64
// * int64
// * string
// * uint64
// * time.Time
// * map[string]interface{} (where interface{} is any of this list)
// * []interface{} (where interface{} is any of this list)
func (t *Tree) ToMap() map[string]interface{} {
result := map[string]interface{}{}
for k, v := range t.values {
switch node := v.(type) {
case []*Tree:
var array []interface{}
for _, item := range node {
array = append(array, item.ToMap())
}
result[k] = array
case *Tree:
result[k] = node.ToMap()
case *tomlValue:
result[k] = tomlValueToGo(node.value)
}
}
return result
}
func tomlValueToGo(v interface{}) interface{} {
if tree, ok := v.(*Tree); ok {
return tree.ToMap()
}
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Slice {
return v
}
values := make([]interface{}, rv.Len())
for i := 0; i < rv.Len(); i++ {
item := rv.Index(i).Interface()
values[i] = tomlValueToGo(item)
}
return values
}
+486
View File
@@ -0,0 +1,486 @@
package toml
import (
"bytes"
"errors"
"fmt"
"reflect"
"strings"
"testing"
"time"
)
type failingWriter struct {
failAt int
written int
buffer bytes.Buffer
}
func (f *failingWriter) Write(p []byte) (n int, err error) {
count := len(p)
toWrite := f.failAt - (count + f.written)
if toWrite < 0 {
toWrite = 0
}
if toWrite > count {
f.written += count
f.buffer.Write(p)
return count, nil
}
f.buffer.Write(p[:toWrite])
f.written = f.failAt
return toWrite, fmt.Errorf("failingWriter failed after writing %d bytes", f.written)
}
func assertErrorString(t *testing.T, expected string, err error) {
expectedErr := errors.New(expected)
if err == nil || err.Error() != expectedErr.Error() {
t.Errorf("expecting error %s, but got %s instead", expected, err)
}
}
func TestTreeWriteToEmptyTable(t *testing.T) {
doc := `[[empty-tables]]
[[empty-tables]]`
toml, err := Load(doc)
if err != nil {
t.Fatal("Unexpected Load error:", err)
}
tomlString, err := toml.ToTomlString()
if err != nil {
t.Fatal("Unexpected ToTomlString error:", err)
}
expected := `
[[empty-tables]]
[[empty-tables]]
`
if tomlString != expected {
t.Fatalf("Expected:\n%s\nGot:\n%s", expected, tomlString)
}
}
func TestTreeWriteToTomlString(t *testing.T) {
toml, err := Load(`name = { first = "Tom", last = "Preston-Werner" }
points = { x = 1, y = 2 }`)
if err != nil {
t.Fatal("Unexpected error:", err)
}
tomlString, _ := toml.ToTomlString()
reparsedTree, err := Load(tomlString)
assertTree(t, reparsedTree, err, map[string]interface{}{
"name": map[string]interface{}{
"first": "Tom",
"last": "Preston-Werner",
},
"points": map[string]interface{}{
"x": int64(1),
"y": int64(2),
},
})
}
func TestTreeWriteToTomlStringSimple(t *testing.T) {
tree, err := Load("[foo]\n\n[[foo.bar]]\na = 42\n\n[[foo.bar]]\na = 69\n")
if err != nil {
t.Errorf("Test failed to parse: %v", err)
return
}
result, err := tree.ToTomlString()
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
expected := "\n[foo]\n\n [[foo.bar]]\n a = 42\n\n [[foo.bar]]\n a = 69\n"
if result != expected {
t.Errorf("Expected got '%s', expected '%s'", result, expected)
}
}
func TestTreeWriteToTomlStringKeysOrders(t *testing.T) {
for i := 0; i < 100; i++ {
tree, _ := Load(`
foobar = true
bar = "baz"
foo = 1
[qux]
foo = 1
bar = "baz2"`)
stringRepr, _ := tree.ToTomlString()
t.Log("Intermediate string representation:")
t.Log(stringRepr)
r := strings.NewReader(stringRepr)
toml, err := LoadReader(r)
if err != nil {
t.Fatal("Unexpected error:", err)
}
assertTree(t, toml, err, map[string]interface{}{
"foobar": true,
"bar": "baz",
"foo": 1,
"qux": map[string]interface{}{
"foo": 1,
"bar": "baz2",
},
})
}
}
func testMaps(t *testing.T, actual, expected map[string]interface{}) {
if !reflect.DeepEqual(actual, expected) {
t.Fatal("trees aren't equal.\n", "Expected:\n", expected, "\nActual:\n", actual)
}
}
func TestTreeWriteToMapSimple(t *testing.T) {
tree, _ := Load("a = 42\nb = 17")
expected := map[string]interface{}{
"a": int64(42),
"b": int64(17),
}
testMaps(t, tree.ToMap(), expected)
}
func TestTreeWriteToInvalidTreeSimpleValue(t *testing.T) {
tree := Tree{values: map[string]interface{}{"foo": int8(1)}}
_, err := tree.ToTomlString()
assertErrorString(t, "invalid value type at foo: int8", err)
}
func TestTreeWriteToInvalidTreeTomlValue(t *testing.T) {
tree := Tree{values: map[string]interface{}{"foo": &tomlValue{value: int8(1), comment: "", position: Position{}}}}
_, err := tree.ToTomlString()
assertErrorString(t, "unsupported value type int8: 1", err)
}
func TestTreeWriteToInvalidTreeTomlValueArray(t *testing.T) {
tree := Tree{values: map[string]interface{}{"foo": &tomlValue{value: int8(1), comment: "", position: Position{}}}}
_, err := tree.ToTomlString()
assertErrorString(t, "unsupported value type int8: 1", err)
}
func TestTreeWriteToFailingWriterInSimpleValue(t *testing.T) {
toml, _ := Load(`a = 2`)
writer := failingWriter{failAt: 0, written: 0}
_, err := toml.WriteTo(&writer)
assertErrorString(t, "failingWriter failed after writing 0 bytes", err)
}
func TestTreeWriteToFailingWriterInTable(t *testing.T) {
toml, _ := Load(`
[b]
a = 2`)
writer := failingWriter{failAt: 2, written: 0}
_, err := toml.WriteTo(&writer)
assertErrorString(t, "failingWriter failed after writing 2 bytes", err)
writer = failingWriter{failAt: 13, written: 0}
_, err = toml.WriteTo(&writer)
assertErrorString(t, "failingWriter failed after writing 13 bytes", err)
}
func TestTreeWriteToFailingWriterInArray(t *testing.T) {
toml, _ := Load(`
[[b]]
a = 2`)
writer := failingWriter{failAt: 2, written: 0}
_, err := toml.WriteTo(&writer)
assertErrorString(t, "failingWriter failed after writing 2 bytes", err)
writer = failingWriter{failAt: 15, written: 0}
_, err = toml.WriteTo(&writer)
assertErrorString(t, "failingWriter failed after writing 15 bytes", err)
}
func TestTreeWriteToMapExampleFile(t *testing.T) {
tree, _ := LoadFile("example.toml")
expected := map[string]interface{}{
"title": "TOML Example",
"owner": map[string]interface{}{
"name": "Tom Preston-Werner",
"organization": "GitHub",
"bio": "GitHub Cofounder & CEO\nLikes tater tots and beer.",
"dob": time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC),
},
"database": map[string]interface{}{
"server": "192.168.1.1",
"ports": []interface{}{int64(8001), int64(8001), int64(8002)},
"connection_max": int64(5000),
"enabled": true,
},
"servers": map[string]interface{}{
"alpha": map[string]interface{}{
"ip": "10.0.0.1",
"dc": "eqdc10",
},
"beta": map[string]interface{}{
"ip": "10.0.0.2",
"dc": "eqdc10",
},
},
"clients": map[string]interface{}{
"data": []interface{}{
[]interface{}{"gamma", "delta"},
[]interface{}{int64(1), int64(2)},
},
"score": 4e-08,
},
}
testMaps(t, tree.ToMap(), expected)
}
func TestTreeWriteToMapWithTablesInMultipleChunks(t *testing.T) {
tree, _ := Load(`
[[menu.main]]
a = "menu 1"
b = "menu 2"
[[menu.main]]
c = "menu 3"
d = "menu 4"`)
expected := map[string]interface{}{
"menu": map[string]interface{}{
"main": []interface{}{
map[string]interface{}{"a": "menu 1", "b": "menu 2"},
map[string]interface{}{"c": "menu 3", "d": "menu 4"},
},
},
}
treeMap := tree.ToMap()
testMaps(t, treeMap, expected)
}
func TestTreeWriteToMapWithArrayOfInlineTables(t *testing.T) {
tree, _ := Load(`
[params]
language_tabs = [
{ key = "shell", name = "Shell" },
{ key = "ruby", name = "Ruby" },
{ key = "python", name = "Python" }
]`)
expected := map[string]interface{}{
"params": map[string]interface{}{
"language_tabs": []interface{}{
map[string]interface{}{
"key": "shell",
"name": "Shell",
},
map[string]interface{}{
"key": "ruby",
"name": "Ruby",
},
map[string]interface{}{
"key": "python",
"name": "Python",
},
},
},
}
treeMap := tree.ToMap()
testMaps(t, treeMap, expected)
}
func TestTreeWriteToMapWithTableInMixedArray(t *testing.T) {
tree, _ := Load(`a = [
"foo",
[
"bar",
{baz = "quux"},
],
[
{a = "b"},
{c = "d"},
],
]`)
expected := map[string]interface{}{
"a": []interface{}{
"foo",
[]interface{}{
"bar",
map[string]interface{}{
"baz": "quux",
},
},
[]interface{}{
map[string]interface{}{
"a": "b",
},
map[string]interface{}{
"c": "d",
},
},
},
}
treeMap := tree.ToMap()
testMaps(t, treeMap, expected)
}
func TestTreeWriteToFloat(t *testing.T) {
tree, err := Load(`a = 3.0`)
if err != nil {
t.Fatal(err)
}
str, err := tree.ToTomlString()
if err != nil {
t.Fatal(err)
}
expected := `a = 3.0`
if strings.TrimSpace(str) != strings.TrimSpace(expected) {
t.Fatalf("Expected:\n%s\nGot:\n%s", expected, str)
}
}
func TestTreeWriteToSpecialFloat(t *testing.T) {
expected := `a = +inf
b = -inf
c = nan`
tree, err := Load(expected)
if err != nil {
t.Fatal(err)
}
str, err := tree.ToTomlString()
if err != nil {
t.Fatal(err)
}
if strings.TrimSpace(str) != strings.TrimSpace(expected) {
t.Fatalf("Expected:\n%s\nGot:\n%s", expected, str)
}
}
func TestOrderedEmptyTrees(t *testing.T) {
type val struct {
Key string `toml:"key"`
}
type structure struct {
First val `toml:"first"`
Empty []val `toml:"empty"`
}
input := structure{First: val{Key: "value"}}
buf := new(bytes.Buffer)
err := NewEncoder(buf).Order(OrderPreserve).Encode(input)
if err != nil {
t.Fatal("failed to encode input")
}
expected := `
[first]
key = "value"
`
if expected != buf.String() {
t.Fatal("expected and encoded body aren't equal: ", expected, buf.String())
}
}
func TestOrderedNonIncreasedLine(t *testing.T) {
type NiceMap map[string]string
type Manifest struct {
NiceMap `toml:"dependencies"`
Build struct {
BuildCommand string `toml:"build-command"`
} `toml:"build"`
}
test := &Manifest{}
test.Build.BuildCommand = "test"
buf := new(bytes.Buffer)
if err := NewEncoder(buf).Order(OrderPreserve).Encode(test); err != nil {
panic(err)
}
expected := `
[dependencies]
[build]
build-command = "test"
`
if expected != buf.String() {
t.Fatal("expected and encoded body aren't equal: ", expected, buf.String())
}
}
func TestIssue290(t *testing.T) {
tomlString :=
`[table]
"127.0.0.1" = "value"
"127.0.0.1:8028" = "value"
"character encoding" = "value"
"ʎǝʞ" = "value"`
t1, err := Load(tomlString)
if err != nil {
t.Fatal("load err:", err)
}
s, err := t1.ToTomlString()
if err != nil {
t.Fatal("ToTomlString err:", err)
}
_, err = Load(s)
if err != nil {
t.Fatal("reload err:", err)
}
}
func BenchmarkTreeToTomlString(b *testing.B) {
toml, err := Load(sampleHard)
if err != nil {
b.Fatal("Unexpected error:", err)
}
for i := 0; i < b.N; i++ {
_, err := toml.ToTomlString()
if err != nil {
b.Fatal(err)
}
}
}
var sampleHard = `# Test file for TOML
# Only this one tries to emulate a TOML file written by a user of the kind of parser writers probably hate
# This part you'll really hate
[the]
test_string = "You'll hate me after this - #" # " Annoying, isn't it?
[the.hard]
test_array = [ "] ", " # "] # ] There you go, parse this!
test_array2 = [ "Test #11 ]proved that", "Experiment #9 was a success" ]
# You didn't think it'd as easy as chucking out the last #, did you?
another_test_string = " Same thing, but with a string #"
harder_test_string = " And when \"'s are in the string, along with # \"" # "and comments are there too"
# Things will get harder
[the.hard."bit#"]
"what?" = "You don't think some user won't do that?"
multi_line_array = [
"]",
# ] Oh yes I did
]
# Each of the following keygroups/key value pairs should produce an error. Uncomment to them to test
#[error] if you didn't catch this, your parser is broken
#string = "Anything other than tabs, spaces and newline after a keygroup or key value pair has ended should produce an error unless it is a comment" like this
#array = [
# "This might most likely happen in multiline arrays",
# Like here,
# "or here,
# and here"
# ] End of array comment, forgot the #
#number = 3.14 pi <--again forgot the # `
+6
View File
@@ -0,0 +1,6 @@
package toml
// ValueStringRepresentation transforms an interface{} value into its toml string representation.
func ValueStringRepresentation(v interface{}, commented string, indent string, ord MarshalOrder, arraysOneElementPerLine bool) (string, error) {
return tomlValueStringRepresentation(v, commented, indent, ord, arraysOneElementPerLine)
}
-427
View File
@@ -1,427 +0,0 @@
package toml
import (
"encoding"
"fmt"
"io"
"io/ioutil"
"reflect"
"time"
"github.com/pelletier/go-toml/v2/internal/ast"
"github.com/pelletier/go-toml/v2/internal/tracker"
)
func Unmarshal(data []byte, v interface{}) error {
p := parser{}
p.Reset(data)
d := decoder{}
return d.FromParser(&p, v)
}
// Decoder reads and decode a TOML document from an input stream.
type Decoder struct {
r io.Reader
}
// NewDecoder creates a new Decoder that will read from r.
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{r: r}
}
// Decode the whole content of r into v.
//
// When a TOML local date is decoded into a time.Time, its value is represented
// in time.Local timezone.
//
// Empty tables decoded in an interface{} create an empty initialized
// map[string]interface{}.
func (d *Decoder) Decode(v interface{}) error {
b, err := ioutil.ReadAll(d.r)
if err != nil {
return err
}
p := parser{}
p.Reset(b)
dec := decoder{}
return dec.FromParser(&p, v)
}
type decoder struct {
// Tracks position in Go arrays.
arrayIndexes map[reflect.Value]int
// Tracks keys that have been seen, with which type.
seen tracker.Seen
}
func (d *decoder) arrayIndex(append bool, v reflect.Value) int {
if d.arrayIndexes == nil {
d.arrayIndexes = make(map[reflect.Value]int, 1)
}
idx, ok := d.arrayIndexes[v]
if !ok {
d.arrayIndexes[v] = 0
} else if append {
idx++
d.arrayIndexes[v] = idx
}
return idx
}
func (d *decoder) FromParser(p *parser, v interface{}) error {
err := d.fromParser(p, v)
if err != nil {
de, ok := err.(*decodeError)
if ok {
err = wrapDecodeError(p.data, de)
}
}
return err
}
func (d *decoder) fromParser(p *parser, v interface{}) error {
r := reflect.ValueOf(v)
if r.Kind() != reflect.Ptr {
return fmt.Errorf("need to target a pointer, not %s", r.Kind())
}
if r.IsNil() {
return fmt.Errorf("target pointer must be non-nil")
}
var skipUntilTable bool
var root target = valueTarget(r.Elem())
current := root
for p.NextExpression() {
node := p.Expression()
if node.Kind == ast.KeyValue && skipUntilTable {
continue
}
err := d.seen.CheckExpression(node)
if err != nil {
return err
}
var found bool
switch node.Kind {
case ast.KeyValue:
err = d.unmarshalKeyValue(current, node)
found = true
case ast.Table:
current, found, err = d.scopeWithKey(root, node.Key())
if err == nil && found {
// In case this table points to an interface,
// make sure it at least holds something that
// looks like a table. Otherwise the information
// of a table is lost, and marshal cannot do the
// round trip.
ensureMapIfInterface(current)
}
case ast.ArrayTable:
current, found, err = d.scopeWithArrayTable(root, node.Key())
default:
panic(fmt.Errorf("this should not be a top level node type: %s", node.Kind))
}
if err != nil {
return err
}
if !found {
skipUntilTable = true
}
}
return p.Error()
}
// scopeWithKey performs target scoping when unmarshaling an ast.KeyValue node.
//
// The goal is to hop from target to target recursively using the names in key.
// Parts of the key should be used to resolve field names for structs, and as
// keys when targeting maps.
//
// When encountering slices, it should always use its last element, and error
// if the slice does not have any.
func (d *decoder) scopeWithKey(x target, key ast.Iterator) (target, bool, error) {
var err error
found := true
for key.Next() {
n := key.Node()
x, found, err = d.scopeTableTarget(false, x, string(n.Data))
if err != nil || !found {
return nil, found, err
}
}
return x, true, nil
}
// scopeWithArrayTable performs target scoping when unmarshaling an
// ast.ArrayTable node.
//
// It is the same as scopeWithKey, but when scoping the last part of the key
// it creates a new element in the array instead of using the last one.
func (d *decoder) scopeWithArrayTable(x target, key ast.Iterator) (target, bool, error) {
var err error
found := true
for key.Next() {
n := key.Node()
if !n.Next().Valid() { // want to stop at one before last
break
}
x, found, err = d.scopeTableTarget(false, x, string(n.Data))
if err != nil || !found {
return nil, found, err
}
}
n := key.Node()
x, found, err = d.scopeTableTarget(false, x, string(n.Data))
if err != nil || !found {
return x, found, err
}
v := x.get()
if v.Kind() == reflect.Ptr {
x, err = scopePtr(x)
if err != nil {
return x, false, err
}
v = x.get()
}
if v.Kind() == reflect.Interface {
x, err = scopeInterface(true, x)
if err != nil {
return x, found, err
}
v = x.get()
}
switch v.Kind() {
case reflect.Slice:
x, err = scopeSlice(true, x)
case reflect.Array:
x, err = d.scopeArray(true, x)
}
return x, found, err
}
func (d *decoder) unmarshalKeyValue(x target, node ast.Node) error {
assertNode(ast.KeyValue, node)
x, found, err := d.scopeWithKey(x, node.Key())
if err != nil {
return err
}
// A struct in the path was not found. Skip this value.
if !found {
return nil
}
return d.unmarshalValue(x, node.Value())
}
var textUnmarshalerType = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem()
func tryTextUnmarshaler(x target, node ast.Node) (bool, error) {
v := x.get()
if v.Kind() != reflect.Struct {
return false, nil
}
// Special case for time, becase we allow to unmarshal to it from
// different kind of AST nodes.
if v.Type() == timeType {
return false, nil
}
if v.Type().Implements(textUnmarshalerType) {
return true, v.Interface().(encoding.TextUnmarshaler).UnmarshalText(node.Data)
}
if v.CanAddr() && v.Addr().Type().Implements(textUnmarshalerType) {
return true, v.Addr().Interface().(encoding.TextUnmarshaler).UnmarshalText(node.Data)
}
return false, nil
}
func (d *decoder) unmarshalValue(x target, node ast.Node) error {
v := x.get()
if v.Kind() == reflect.Ptr {
if !v.Elem().IsValid() {
err := x.set(reflect.New(v.Type().Elem()))
if err != nil {
return err
}
v = x.get()
}
return d.unmarshalValue(valueTarget(v.Elem()), node)
}
ok, err := tryTextUnmarshaler(x, node)
if ok {
return err
}
switch node.Kind {
case ast.String:
return unmarshalString(x, node)
case ast.Bool:
return unmarshalBool(x, node)
case ast.Integer:
return unmarshalInteger(x, node)
case ast.Float:
return unmarshalFloat(x, node)
case ast.Array:
return d.unmarshalArray(x, node)
case ast.InlineTable:
return d.unmarshalInlineTable(x, node)
case ast.LocalDateTime:
return unmarshalLocalDateTime(x, node)
case ast.DateTime:
return unmarshalDateTime(x, node)
case ast.LocalDate:
return unmarshalLocalDate(x, node)
default:
panic(fmt.Errorf("unhandled unmarshalValue kind %s", node.Kind))
}
}
func unmarshalLocalDate(x target, node ast.Node) error {
assertNode(ast.LocalDate, node)
v, err := parseLocalDate(node.Data)
if err != nil {
return err
}
return setDate(x, v)
}
func unmarshalLocalDateTime(x target, node ast.Node) error {
assertNode(ast.LocalDateTime, node)
v, rest, err := parseLocalDateTime(node.Data)
if err != nil {
return err
}
if len(rest) > 0 {
return newDecodeError(rest, "extra characters at the end of a local date time")
}
return setLocalDateTime(x, v)
}
func unmarshalDateTime(x target, node ast.Node) error {
assertNode(ast.DateTime, node)
v, err := parseDateTime(node.Data)
if err != nil {
return err
}
return setDateTime(x, v)
}
func setLocalDateTime(x target, v LocalDateTime) error {
return x.set(reflect.ValueOf(v))
}
func setDateTime(x target, v time.Time) error {
return x.set(reflect.ValueOf(v))
}
var timeType = reflect.TypeOf(time.Time{})
func setDate(x target, v LocalDate) error {
if x.get().Type() == timeType {
cast := v.In(time.Local)
return setDateTime(x, cast)
}
return x.set(reflect.ValueOf(v))
}
func unmarshalString(x target, node ast.Node) error {
assertNode(ast.String, node)
return setString(x, string(node.Data))
}
func unmarshalBool(x target, node ast.Node) error {
assertNode(ast.Bool, node)
v := node.Data[0] == 't'
return setBool(x, v)
}
func unmarshalInteger(x target, node ast.Node) error {
assertNode(ast.Integer, node)
v, err := parseInteger(node.Data)
if err != nil {
return err
}
return setInt64(x, v)
}
func unmarshalFloat(x target, node ast.Node) error {
assertNode(ast.Float, node)
v, err := parseFloat(node.Data)
if err != nil {
return err
}
return setFloat64(x, v)
}
func (d *decoder) unmarshalInlineTable(x target, node ast.Node) error {
assertNode(ast.InlineTable, node)
ensureMapIfInterface(x)
it := node.Children()
for it.Next() {
n := it.Node()
err := d.unmarshalKeyValue(x, n)
if err != nil {
return err
}
}
return nil
}
func (d *decoder) unmarshalArray(x target, node ast.Node) error {
assertNode(ast.Array, node)
err := ensureValueIndexable(x)
if err != nil {
return err
}
it := node.Children()
idx := 0
for it.Next() {
n := it.Node()
v, err := elementAt(x, idx)
if err != nil {
return err
}
if v == nil {
// when we go out of bound for an array just stop processing it to
// mimic encoding/json
break
}
err = d.unmarshalValue(v, n)
if err != nil {
return err
}
idx++
}
return nil
}
func assertNode(expected ast.Kind, node ast.Node) {
if node.Kind != expected {
panic(fmt.Errorf("expected node of kind %s, not %s", expected, node.Kind))
}
}
-926
View File
@@ -1,926 +0,0 @@
package toml_test
import (
"math"
"strconv"
"testing"
"time"
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUnmarshal_Integers(t *testing.T) {
examples := []struct {
desc string
input string
expected int64
err bool
}{
{
desc: "integer just digits",
input: `1234`,
expected: 1234,
},
{
desc: "integer zero",
input: `0`,
expected: 0,
},
{
desc: "integer sign",
input: `+99`,
expected: 99,
},
{
desc: "integer hex uppercase",
input: `0xDEADBEEF`,
expected: 0xDEADBEEF,
},
{
desc: "integer hex lowercase",
input: `0xdead_beef`,
expected: 0xDEADBEEF,
},
{
desc: "integer octal",
input: `0o01234567`,
expected: 0o01234567,
},
{
desc: "integer binary",
input: `0b11010110`,
expected: 0b11010110,
},
}
type doc struct {
A int64
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
doc := doc{}
err := toml.Unmarshal([]byte(`A = `+e.input), &doc)
require.NoError(t, err)
assert.Equal(t, e.expected, doc.A)
})
}
}
func TestUnmarshal_Floats(t *testing.T) {
examples := []struct {
desc string
input string
expected float64
testFn func(t *testing.T, v float64)
err bool
}{
{
desc: "float pi",
input: `3.1415`,
expected: 3.1415,
},
{
desc: "float negative",
input: `-0.01`,
expected: -0.01,
},
{
desc: "float signed exponent",
input: `5e+22`,
expected: 5e+22,
},
{
desc: "float exponent lowercase",
input: `1e06`,
expected: 1e06,
},
{
desc: "float exponent uppercase",
input: `-2E-2`,
expected: -2e-2,
},
{
desc: "float fractional with exponent",
input: `6.626e-34`,
expected: 6.626e-34,
},
{
desc: "float underscores",
input: `224_617.445_991_228`,
expected: 224_617.445_991_228,
},
{
desc: "inf",
input: `inf`,
expected: math.Inf(+1),
},
{
desc: "inf negative",
input: `-inf`,
expected: math.Inf(-1),
},
{
desc: "inf positive",
input: `+inf`,
expected: math.Inf(+1),
},
{
desc: "nan",
input: `nan`,
testFn: func(t *testing.T, v float64) {
assert.True(t, math.IsNaN(v))
},
},
{
desc: "nan negative",
input: `-nan`,
testFn: func(t *testing.T, v float64) {
assert.True(t, math.IsNaN(v))
},
},
{
desc: "nan positive",
input: `+nan`,
testFn: func(t *testing.T, v float64) {
assert.True(t, math.IsNaN(v))
},
},
}
type doc struct {
A float64
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
doc := doc{}
err := toml.Unmarshal([]byte(`A = `+e.input), &doc)
require.NoError(t, err)
if e.testFn != nil {
e.testFn(t, doc.A)
} else {
assert.Equal(t, e.expected, doc.A)
}
})
}
}
func TestUnmarshal(t *testing.T) {
type test struct {
target interface{}
expected interface{}
err bool
}
examples := []struct {
skip bool
desc string
input string
gen func() test
}{
{
desc: "kv string",
input: `A = "foo"`,
gen: func() test {
type doc struct {
A string
}
return test{
target: &doc{},
expected: &doc{A: "foo"},
}
},
},
{
desc: "issue 475 - space between dots in key",
input: `fruit. color = "yellow"
fruit . flavor = "banana"`,
gen: func() test {
m := map[string]interface{}{}
return test{
target: &m,
expected: &map[string]interface{}{
"fruit": map[string]interface{}{
"color": "yellow",
"flavor": "banana",
},
},
}
},
},
{
desc: "issue 427 - quotation marks in key",
input: `'"a"' = 1
"\"b\"" = 2`,
gen: func() test {
m := map[string]interface{}{}
return test{
target: &m,
expected: &map[string]interface{}{
`"a"`: int64(1),
`"b"`: int64(2),
},
}
},
},
{
desc: "multiline basic string",
input: `A = """\
Test"""`,
gen: func() test {
type doc struct {
A string
}
return test{
target: &doc{},
expected: &doc{A: "Test"},
}
},
},
{
desc: "kv bool true",
input: `A = true`,
gen: func() test {
type doc struct {
A bool
}
return test{
target: &doc{},
expected: &doc{A: true},
}
},
},
{
desc: "kv bool false",
input: `A = false`,
gen: func() test {
type doc struct {
A bool
}
return test{
target: &doc{A: true},
expected: &doc{A: false},
}
},
},
{
desc: "string array",
input: `A = ["foo", "bar"]`,
gen: func() test {
type doc struct {
A []string
}
return test{
target: &doc{},
expected: &doc{A: []string{"foo", "bar"}},
}
},
},
{
desc: "standard table",
input: `[A]
B = "data"`,
gen: func() test {
type A struct {
B string
}
type doc struct {
A A
}
return test{
target: &doc{},
expected: &doc{A: A{B: "data"}},
}
},
},
{
desc: "standard empty table",
input: `[A]`,
gen: func() test {
var v map[string]interface{}
return test{
target: &v,
expected: &map[string]interface{}{`A`: map[string]interface{}{}},
}
},
},
{
desc: "inline table",
input: `Name = {First = "hello", Last = "world"}`,
gen: func() test {
type name struct {
First string
Last string
}
type doc struct {
Name name
}
return test{
target: &doc{},
expected: &doc{Name: name{
First: "hello",
Last: "world",
}},
}
},
},
{
desc: "inline empty table",
input: `A = {}`,
gen: func() test {
var v map[string]interface{}
return test{
target: &v,
expected: &map[string]interface{}{`A`: map[string]interface{}{}},
}
},
},
{
desc: "inline table inside array",
input: `Names = [{First = "hello", Last = "world"}, {First = "ab", Last = "cd"}]`,
gen: func() test {
type name struct {
First string
Last string
}
type doc struct {
Names []name
}
return test{
target: &doc{},
expected: &doc{
Names: []name{
{
First: "hello",
Last: "world",
},
{
First: "ab",
Last: "cd",
},
},
},
}
},
},
{
desc: "into map[string]interface{}",
input: `A = "foo"`,
gen: func() test {
doc := map[string]interface{}{}
return test{
target: &doc,
expected: &map[string]interface{}{
"A": "foo",
},
}
},
},
{
desc: "multi keys of different types into map[string]interface{}",
input: `A = "foo"
B = 42`,
gen: func() test {
doc := map[string]interface{}{}
return test{
target: &doc,
expected: &map[string]interface{}{
"A": "foo",
"B": int64(42),
},
}
},
},
{
desc: "slice in a map[string]interface{}",
input: `A = ["foo", "bar"]`,
gen: func() test {
doc := map[string]interface{}{}
return test{
target: &doc,
expected: &map[string]interface{}{
"A": []interface{}{"foo", "bar"},
},
}
},
},
{
desc: "string into map[string]string",
input: `A = "foo"`,
gen: func() test {
doc := map[string]string{}
return test{
target: &doc,
expected: &map[string]string{
"A": "foo",
},
}
},
},
{
desc: "float64 into map[string]string",
input: `A = 42.0`,
gen: func() test {
doc := map[string]string{}
return test{
target: &doc,
err: true,
}
},
},
{
desc: "one-level one-element array table",
input: `[[First]]
Second = "hello"`,
gen: func() test {
type First struct {
Second string
}
type Doc struct {
First []First
}
return test{
target: &Doc{},
expected: &Doc{
First: []First{
{
Second: "hello",
},
},
},
}
},
},
{
desc: "one-level multi-element array table",
input: `[[Products]]
Name = "Hammer"
Sku = 738594937
[[Products]] # empty table within the array
[[Products]]
Name = "Nail"
Sku = 284758393
Color = "gray"`,
gen: func() test {
type Product struct {
Name string
Sku int64
Color string
}
type Doc struct {
Products []Product
}
return test{
target: &Doc{},
expected: &Doc{
Products: []Product{
{Name: "Hammer", Sku: 738594937},
{},
{Name: "Nail", Sku: 284758393, Color: "gray"},
},
},
}
},
},
{
desc: "one-level multi-element array table to map",
input: `[[Products]]
Name = "Hammer"
Sku = 738594937
[[Products]] # empty table within the array
[[Products]]
Name = "Nail"
Sku = 284758393
Color = "gray"`,
gen: func() test {
return test{
target: &map[string]interface{}{},
expected: &map[string]interface{}{
"Products": []interface{}{
map[string]interface{}{
"Name": "Hammer",
"Sku": int64(738594937),
},
nil,
map[string]interface{}{
"Name": "Nail",
"Sku": int64(284758393),
"Color": "gray",
},
},
},
}
},
},
{
desc: "sub-table in array table",
input: `[[Fruits]]
Name = "apple"
[Fruits.Physical] # subtable
Color = "red"
Shape = "round"`,
gen: func() test {
return test{
target: &map[string]interface{}{},
expected: &map[string]interface{}{
"Fruits": []interface{}{
map[string]interface{}{
"Name": "apple",
"Physical": map[string]interface{}{
"Color": "red",
"Shape": "round",
},
},
},
},
}
},
},
{
desc: "multiple sub-table in array tables",
input: `[[Fruits]]
Name = "apple"
[[Fruits.Varieties]] # nested array of tables
Name = "red delicious"
[[Fruits.Varieties]]
Name = "granny smith"
[[Fruits]]
Name = "banana"
[[Fruits.Varieties]]
Name = "plantain"`,
gen: func() test {
return test{
target: &map[string]interface{}{},
expected: &map[string]interface{}{
"Fruits": []interface{}{
map[string]interface{}{
"Name": "apple",
"Varieties": []interface{}{
map[string]interface{}{
"Name": "red delicious",
},
map[string]interface{}{
"Name": "granny smith",
},
},
},
map[string]interface{}{
"Name": "banana",
"Varieties": []interface{}{
map[string]interface{}{
"Name": "plantain",
},
},
},
},
},
}
},
},
{
desc: "multiple sub-table in array tables into structs",
input: `[[Fruits]]
Name = "apple"
[[Fruits.Varieties]] # nested array of tables
Name = "red delicious"
[[Fruits.Varieties]]
Name = "granny smith"
[[Fruits]]
Name = "banana"
[[Fruits.Varieties]]
Name = "plantain"`,
gen: func() test {
type Variety struct {
Name string
}
type Fruit struct {
Name string
Varieties []Variety
}
type doc struct {
Fruits []Fruit
}
return test{
target: &doc{},
expected: &doc{
Fruits: []Fruit{
{
Name: "apple",
Varieties: []Variety{
{Name: "red delicious"},
{Name: "granny smith"},
},
},
{
Name: "banana",
Varieties: []Variety{
{Name: "plantain"},
},
},
},
},
}
},
},
{
desc: "slice pointer in slice pointer",
input: `A = ["Hello"]`,
gen: func() test {
type doc struct {
A *[]*string
}
hello := "Hello"
return test{
target: &doc{},
expected: &doc{
A: &[]*string{&hello},
},
}
},
},
{
desc: "interface holding a struct",
input: `[A]
B = "After"`,
gen: func() test {
type inner struct {
B interface{}
}
type doc struct {
A interface{}
}
return test{
target: &doc{
A: inner{
B: "Before",
},
},
expected: &doc{
A: map[string]interface{}{
"B": "After",
},
},
}
},
},
{
desc: "array of structs with table arrays",
input: `[[A]]
B = "one"
[[A]]
B = "two"`,
gen: func() test {
type inner struct {
B string
}
type doc struct {
A [4]inner
}
return test{
target: &doc{},
expected: &doc{
A: [4]inner{
{B: "one"},
{B: "two"},
},
},
}
},
},
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
if e.skip {
t.Skip()
}
test := e.gen()
if test.err && test.expected != nil {
panic("invalid test: cannot expect both an error and a value")
}
err := toml.Unmarshal([]byte(e.input), test.target)
if test.err {
require.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, test.target)
}
})
}
}
type Integer484 struct {
Value int
}
func (i Integer484) MarshalText() ([]byte, error) {
return []byte(strconv.Itoa(i.Value)), nil
}
func (i *Integer484) UnmarshalText(data []byte) error {
conv, err := strconv.Atoi(string(data))
if err != nil {
return err
}
i.Value = conv
return nil
}
type Config484 struct {
Integers []Integer484 `toml:"integers"`
}
func TestIssue484(t *testing.T) {
raw := []byte(`integers = ["1","2","3","100"]`)
var cfg Config484
err := toml.Unmarshal(raw, &cfg)
require.NoError(t, err)
assert.Equal(t, Config484{
Integers: []Integer484{{1}, {2}, {3}, {100}},
}, cfg)
}
type Map458 map[string]interface{}
type Slice458 []interface{}
func (m Map458) A(s string) Slice458 {
return m[s].([]interface{})
}
func TestIssue458(t *testing.T) {
s := []byte(`[[package]]
dependencies = ["regex"]
name = "decode"
version = "0.1.0"`)
m := Map458{}
err := toml.Unmarshal(s, &m)
require.NoError(t, err)
a := m.A("package")
expected := Slice458{
map[string]interface{}{
"dependencies": []interface{}{"regex"},
"name": "decode",
"version": "0.1.0"},
}
assert.Equal(t, expected, a)
}
func TestIssue252(t *testing.T) {
type config struct {
Val1 string `toml:"val1"`
Val2 string `toml:"val2"`
}
var configFile = []byte(
`
val1 = "test1"
`)
cfg := &config{
Val2: "test2",
}
err := toml.Unmarshal(configFile, cfg)
require.NoError(t, err)
require.Equal(t, "test2", cfg.Val2)
}
func TestIssue494(t *testing.T) {
data := `
foo = 2021-04-08
bar = 2021-04-08
`
type s struct {
Foo time.Time `toml:"foo"`
Bar time.Time `toml:"bar"`
}
ss := new(s)
err := toml.Unmarshal([]byte(data), ss)
require.NoError(t, err)
}
func TestUnmarshalDecodeErrors(t *testing.T) {
examples := []struct {
desc string
data string
msg string
}{
{
desc: "int with wrong base",
data: `a = 0f2`,
},
{
desc: "literal string with new lines",
data: `a = 'hello
world'`,
msg: `literal strings cannot have new lines`,
},
{
desc: "unterminated literal string",
data: `a = 'hello`,
msg: `unterminated literal string`,
},
{
desc: "unterminated multiline literal string",
data: `a = '''hello`,
msg: `multiline literal string not terminated by '''`,
},
{
desc: "basic string with new lines",
data: `a = "hello
"`,
msg: `basic strings cannot have new lines`,
},
{
desc: "basic string with unfinished escape",
data: `a = "hello \`,
msg: `need a character after \`,
},
{
desc: "basic unfinished multiline string",
data: `a = """hello`,
msg: `multiline basic string not terminated by """`,
},
{
desc: "basic unfinished escape in multiline string",
data: `a = """hello \`,
msg: `need a character after \`,
},
{
desc: "malformed local date",
data: `a = 2021-033-0`,
msg: `dates are expected to have the format YYYY-MM-DD`,
},
{
desc: "malformed tz",
data: `a = 2021-03-30 21:31:00+1`,
msg: `invalid date-time timezone`,
},
{
desc: "malformed tz first char",
data: `a = 2021-03-30 21:31:00:1`,
msg: `extra characters at the end of a local date time`,
},
{
desc: "bad char between hours and minutes",
data: `a = 2021-03-30 213:1:00`,
msg: `expecting colon between hours and minutes`,
},
{
desc: "bad char between minutes and seconds",
data: `a = 2021-03-30 21:312:0`,
msg: `expecting colon between minutes and seconds`,
},
}
for _, e := range examples {
t.Run(e.desc, func(t *testing.T) {
m := map[string]interface{}{}
err := toml.Unmarshal([]byte(e.data), &m)
require.Error(t, err)
de, ok := err.(*toml.DecodeError)
if !ok {
t.Fatalf("err should have been a *toml.DecodeError, but got %s (%T)", err, err)
}
if e.msg != "" {
t.Log("\n" + de.String())
require.Equal(t, e.msg, de.Error())
}
})
}
}
func TestIssue287(t *testing.T) {
b := `y=[[{}]]`
v := map[string]interface{}{}
err := toml.Unmarshal([]byte(b), &v)
require.NoError(t, err)
expected := map[string]interface{}{
"y": []interface{}{
[]interface{}{
map[string]interface{}{},
},
},
}
require.Equal(t, expected, v)
}