Compare commits
14 Commits
specialize
..
v1.9.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 8410c965c2 | |||
| d083470585 | |||
| c893dbf25c | |||
| 2a1df71375 | |||
| a2f5197638 | |||
| bb65137dc4 | |||
| 99782c87cf | |||
| ce6fbd7bc0 | |||
| b59c12a70d | |||
| 6a307ac0d0 | |||
| a2e5256180 | |||
| 5163266f16 | |||
| b4f0a950bf | |||
| ef48fb2be1 |
@@ -1,3 +0,0 @@
|
|||||||
* text=auto
|
|
||||||
|
|
||||||
benchmark/benchmark.toml text eol=lf
|
|
||||||
@@ -1,9 +1,18 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
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**
|
**Describe the bug**
|
||||||
A clear and concise description of what the bug is.
|
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".
|
A clear and concise description of what you expected to happen, if other than "should work".
|
||||||
|
|
||||||
**Versions**
|
**Versions**
|
||||||
- go-toml: version (git sha)
|
- go-toml: version (or git sha)
|
||||||
- go: version
|
- go: version
|
||||||
- operating system: e.g. macOS, Windows, Linux
|
- operating system: e.g. macOS, Windows, Linux
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<!--
|
|
||||||
|
|
||||||
Thank you for your pull request!
|
|
||||||
|
|
||||||
Please read the Code changes section of the CONTRIBUTING.md file,
|
|
||||||
and make sure you have followed the instructions.
|
|
||||||
|
|
||||||
https://github.com/pelletier/go-toml/blob/v2/CONTRIBUTING.md#code-changes
|
|
||||||
|
|
||||||
-->
|
|
||||||
|
|
||||||
Explanation of what this pull request does.
|
|
||||||
|
|
||||||
More detailed description of the decisions being made and the reasons why (if
|
|
||||||
the patch is non-trivial).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Paste `benchstat` results here
|
|
||||||
+2
-11
@@ -1,17 +1,8 @@
|
|||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: gomod
|
- package-ecosystem: gomod
|
||||||
directory: /
|
directory: "/"
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
- package-ecosystem: github-actions
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
- package-ecosystem: docker
|
|
||||||
directory: /
|
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
|
time: "13:00"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
changelog:
|
|
||||||
exclude:
|
|
||||||
labels:
|
|
||||||
- build
|
|
||||||
categories:
|
|
||||||
- title: What's new
|
|
||||||
labels:
|
|
||||||
- feature
|
|
||||||
- title: Performance
|
|
||||||
labels:
|
|
||||||
- performance
|
|
||||||
- title: Fixed bugs
|
|
||||||
labels:
|
|
||||||
- bug
|
|
||||||
- title: Documentation
|
|
||||||
labels:
|
|
||||||
- doc
|
|
||||||
- title: Other changes
|
|
||||||
labels:
|
|
||||||
- "*"
|
|
||||||
@@ -13,7 +13,7 @@ name: "CodeQL"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master, v2 ]
|
branches: [ master ]
|
||||||
pull_request:
|
pull_request:
|
||||||
# The branches below must be a subset of the branches above
|
# The branches below must be a subset of the branches above
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
name: coverage
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- v2
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
report:
|
|
||||||
runs-on: "ubuntu-latest"
|
|
||||||
name: report
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@master
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Setup go
|
|
||||||
uses: actions/setup-go@master
|
|
||||||
with:
|
|
||||||
go-version: 1.16
|
|
||||||
- name: Run tests with coverage
|
|
||||||
run: ./ci.sh coverage -d "${GITHUB_BASE_REF-HEAD}"
|
|
||||||
@@ -1,25 +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.16', '1.17' ]
|
|
||||||
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 ./...
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
[service]
|
|
||||||
golangci-lint-version = "1.39.0"
|
|
||||||
|
|
||||||
[linters-settings.wsl]
|
|
||||||
allow-assign-and-anything = true
|
|
||||||
|
|
||||||
[linters-settings.exhaustive]
|
|
||||||
default-signifies-exhaustive = 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"
|
|
||||||
]
|
|
||||||
+63
-113
@@ -1,74 +1,74 @@
|
|||||||
# Contributing
|
## Contributing
|
||||||
|
|
||||||
Thank you for your interest in go-toml! We appreciate you considering
|
Thank you for your interest in go-toml! We appreciate you considering
|
||||||
contributing to go-toml!
|
contributing to go-toml!
|
||||||
|
|
||||||
The main goal is the project is to provide an easy-to-use and efficient TOML
|
The main goal is the project is to provide an easy-to-use TOML
|
||||||
implementation for Go that gets the job done and gets out of your way – dealing
|
implementation for Go that gets the job done and gets out of your way –
|
||||||
with TOML is probably not the central piece of your project.
|
dealing with TOML is probably not the central piece of your project.
|
||||||
|
|
||||||
As the single maintainer of go-toml, time is scarce. All help, big or small, is
|
As the single maintainer of go-toml, time is scarce. All help, big or
|
||||||
more than welcomed!
|
small, is more than welcomed!
|
||||||
|
|
||||||
## Ask questions
|
### Ask questions
|
||||||
|
|
||||||
Any question you may have, somebody else might have it too. Always feel free to
|
Any question you may have, somebody else might have it too. Always feel
|
||||||
ask them on the [discussion board][discussions]. We will try to answer them as
|
free to ask them on the [issues tracker][issues-tracker]. We will try to
|
||||||
clearly and quickly as possible, time permitting.
|
answer them as clearly and quickly as possible, time permitting.
|
||||||
|
|
||||||
Asking questions also helps us identify areas where the documentation needs
|
Asking questions also helps us identify areas where the documentation needs
|
||||||
improvement, or new features that weren't envisioned before. Sometimes, a
|
improvement, or new features that weren't envisioned before. Sometimes, a
|
||||||
seemingly innocent question leads to the fix of a bug. Don't hesitate and ask
|
seemingly innocent question leads to the fix of a bug. Don't hesitate and
|
||||||
away!
|
ask away!
|
||||||
|
|
||||||
[discussions]: https://github.com/pelletier/go-toml/discussions
|
### Improve the documentation
|
||||||
|
|
||||||
## Improve the documentation
|
The best way to share your knowledge and experience with go-toml is to
|
||||||
|
improve the documentation. Fix a typo, clarify an interface, add an
|
||||||
|
example, anything goes!
|
||||||
|
|
||||||
The best way to share your knowledge and experience with go-toml is to improve
|
The documentation is present in the [README][readme] and thorough the
|
||||||
the documentation. Fix a typo, clarify an interface, add an example, anything
|
source code. On release, it gets updated on [pkg.go.dev][pkg.go.dev]. To make a
|
||||||
goes!
|
change to the documentation, create a pull request with your proposed
|
||||||
|
changes. For simple changes like that, the easiest way to go is probably
|
||||||
|
the "Fork this project and edit the file" button on Github, displayed at
|
||||||
|
the top right of the file. Unless it's a trivial change (for example a
|
||||||
|
typo), provide a little bit of context in your pull request description or
|
||||||
|
commit message.
|
||||||
|
|
||||||
The documentation is present in the [README][readme] and thorough the source
|
### Report a bug
|
||||||
code. On release, it gets updated on [pkg.go.dev][pkg.go.dev]. To make a change
|
|
||||||
to the documentation, create a pull request with your proposed changes. For
|
|
||||||
simple changes like that, the easiest way to go is probably the "Fork this
|
|
||||||
project and edit the file" button on Github, displayed at the top right of the
|
|
||||||
file. Unless it's a trivial change (for example a typo), provide a little bit of
|
|
||||||
context in your pull request description or commit message.
|
|
||||||
|
|
||||||
## Report a bug
|
Found a bug! Sorry to hear that :(. Help us and other track them down and
|
||||||
|
fix by reporting it. [File a new bug report][bug-report] on the [issues
|
||||||
|
tracker][issues-tracker]. The template should provide enough guidance on
|
||||||
|
what to include. When in doubt: add more details! By reducing ambiguity and
|
||||||
|
providing more information, it decreases back and forth and saves everyone
|
||||||
|
time.
|
||||||
|
|
||||||
Found a bug! Sorry to hear that :(. Help us and other track them down and fix by
|
### Code changes
|
||||||
reporting it. [File a new bug report][bug-report] on the [issues
|
|
||||||
tracker][issues-tracker]. The template should provide enough guidance on what to
|
|
||||||
include. When in doubt: add more details! By reducing ambiguity and providing
|
|
||||||
more information, it decreases back and forth and saves everyone time.
|
|
||||||
|
|
||||||
## Code changes
|
|
||||||
|
|
||||||
Want to contribute a patch? Very happy to hear that!
|
Want to contribute a patch? Very happy to hear that!
|
||||||
|
|
||||||
First, some high-level rules:
|
First, some high-level rules:
|
||||||
|
|
||||||
- A short proposal with some POC code is better than a lengthy piece of text
|
* A short proposal with some POC code is better than a lengthy piece of
|
||||||
with no code. Code speaks louder than words. That being said, bigger changes
|
text with no code. Code speaks louder than words.
|
||||||
should probably start with a [discussion][discussions].
|
* No backward-incompatible patch will be accepted unless discussed.
|
||||||
- No backward-incompatible patch will be accepted unless discussed. Sometimes
|
Sometimes it's hard, and Go's lack of versioning by default does not
|
||||||
it's hard, but we try not to break people's programs unless we absolutely have
|
help, but we try not to break people's programs unless we absolutely have
|
||||||
to.
|
to.
|
||||||
- If you are writing a new feature or extending an existing one, make sure to
|
* If you are writing a new feature or extending an existing one, make sure
|
||||||
write some documentation.
|
to write some documentation.
|
||||||
- Bug fixes need to be accompanied with regression tests.
|
* Bug fixes need to be accompanied with regression tests.
|
||||||
- New code needs to be tested.
|
* New code needs to be tested.
|
||||||
- Your commit messages need to explain why the change is needed, even if already
|
* Your commit messages need to explain why the change is needed, even if
|
||||||
included in the PR description.
|
already included in the PR description.
|
||||||
|
|
||||||
It does sound like a lot, but those best practices are here to save time overall
|
It does sound like a lot, but those best practices are here to save time
|
||||||
and continuously improve the quality of the project, which is something everyone
|
overall and continuously improve the quality of the project, which is
|
||||||
benefits from.
|
something everyone benefits from.
|
||||||
|
|
||||||
### Get started
|
#### Get started
|
||||||
|
|
||||||
The fairly standard code contribution process looks like that:
|
The fairly standard code contribution process looks like that:
|
||||||
|
|
||||||
@@ -76,92 +76,42 @@ The fairly standard code contribution process looks like that:
|
|||||||
2. Make your changes, commit on any branch you like.
|
2. Make your changes, commit on any branch you like.
|
||||||
3. [Open up a pull request][pull-request]
|
3. [Open up a pull request][pull-request]
|
||||||
4. Review, potential ask for changes.
|
4. Review, potential ask for changes.
|
||||||
5. Merge.
|
5. Merge. You're in!
|
||||||
|
|
||||||
Feel free to ask for help! You can create draft pull requests to gather
|
Feel free to ask for help! You can create draft pull requests to gather
|
||||||
some early feedback!
|
some early feedback!
|
||||||
|
|
||||||
### Run the tests
|
#### Run the tests
|
||||||
|
|
||||||
You can run tests for go-toml using Go's test tool: `go test -race ./...`.
|
You can run tests for go-toml using Go's test tool: `go test ./...`.
|
||||||
|
When creating a pull requests, all tests will be ran on Linux on a few Go
|
||||||
|
versions (Travis CI), and on Windows using the latest Go version
|
||||||
|
(AppVeyor).
|
||||||
|
|
||||||
During the pull request process, all tests will be ran on Linux, Windows, and
|
#### Style
|
||||||
MacOS on the last two versions of Go.
|
|
||||||
|
|
||||||
However, given GitHub's new policy to _not_ run Actions on pull requests until a
|
Try to look around and follow the same format and structure as the rest of
|
||||||
maintainer clicks on button, it is highly recommended that you run them locally
|
the code. We enforce using `go fmt` on the whole code base.
|
||||||
as you make changes.
|
|
||||||
|
|
||||||
### Check coverage
|
|
||||||
|
|
||||||
We use `go tool cover` to compute test coverage. Most code editors have a way to
|
|
||||||
run and display code coverage, but at the end of the day, we do this:
|
|
||||||
|
|
||||||
```
|
|
||||||
go test -covermode=atomic -coverprofile=coverage.out
|
|
||||||
go tool cover -func=coverage.out
|
|
||||||
```
|
|
||||||
|
|
||||||
and verify that the overall percentage of tested code does not go down. This is
|
|
||||||
a requirement. As a rule of thumb, all lines of code touched by your changes
|
|
||||||
should be covered. On Unix you can use `./ci.sh coverage -d v2` to check if your
|
|
||||||
code lowers the coverage.
|
|
||||||
|
|
||||||
### Verify performance
|
|
||||||
|
|
||||||
Go-toml aims to stay efficient. We rely on a set of scenarios executed with Go's
|
|
||||||
builtin benchmark systems. Because of their noisy nature, containers provided by
|
|
||||||
Github Actions cannot be reliably used for benchmarking. As a result, you are
|
|
||||||
responsible for checking that your changes do not incur a performance penalty.
|
|
||||||
You can run their following to execute benchmarks:
|
|
||||||
|
|
||||||
```
|
|
||||||
go test ./... -bench=. -count=10
|
|
||||||
```
|
|
||||||
|
|
||||||
Benchmark results should be compared against each other with
|
|
||||||
[benchstat][benchstat]. Typical flow looks like this:
|
|
||||||
|
|
||||||
1. On the `v2` branch, run `go test ./... -bench=. -count 10` and save output to
|
|
||||||
a file (for example `old.txt`).
|
|
||||||
2. Make some code changes.
|
|
||||||
3. Run `go test ....` again, and save the output to an other file (for example
|
|
||||||
`new.txt`).
|
|
||||||
4. Run `benchstat old.txt new.txt` to check that time/op does not go up in any
|
|
||||||
test.
|
|
||||||
|
|
||||||
On Unix you can use `./ci.sh benchmark -d v2` to verify how your code impacts
|
|
||||||
performance.
|
|
||||||
|
|
||||||
It is highly encouraged to add the benchstat results to your pull request
|
|
||||||
description. Pull requests that lower performance will receive more scrutiny.
|
|
||||||
|
|
||||||
[benchstat]: https://pkg.go.dev/golang.org/x/perf/cmd/benchstat
|
|
||||||
|
|
||||||
### Style
|
|
||||||
|
|
||||||
Try to look around and follow the same format and structure as the rest of the
|
|
||||||
code. We enforce using `go fmt` on the whole code base.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Maintainers-only
|
### Maintainers-only
|
||||||
|
|
||||||
### Merge pull request
|
#### Merge pull request
|
||||||
|
|
||||||
Checklist:
|
Checklist:
|
||||||
|
|
||||||
- Passing CI.
|
* Passing CI.
|
||||||
- Does not introduce backward-incompatible changes (unless discussed).
|
* Does not introduce backward-incompatible changes (unless discussed).
|
||||||
- Has relevant doc changes.
|
* Has relevant doc changes.
|
||||||
- Benchstat does not show performance regression.
|
* Has relevant unit tests.
|
||||||
|
|
||||||
1. Merge using "squash and merge".
|
1. Merge using "squash and merge".
|
||||||
2. Make sure to edit the commit message to keep all the useful information
|
2. Make sure to edit the commit message to keep all the useful information
|
||||||
nice and clean.
|
nice and clean.
|
||||||
3. Make sure the commit title is clear and contains the PR number (#123).
|
3. Make sure the commit title is clear and contains the PR number (#123).
|
||||||
|
|
||||||
### New release
|
#### New release
|
||||||
|
|
||||||
1. Go to [releases][releases]. Click on "X commits to master since this
|
1. Go to [releases][releases]. Click on "X commits to master since this
|
||||||
release".
|
release".
|
||||||
|
|||||||
+11
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
**Issue:** add link to pelletier/go-toml issue here
|
||||||
|
|
||||||
|
Explanation of what this pull request does.
|
||||||
|
|
||||||
|
More detailed description of the decisions being made and the reasons why (if the patch is non-trivial).
|
||||||
@@ -1,426 +1,175 @@
|
|||||||
# go-toml v2
|
# go-toml
|
||||||
|
|
||||||
Go library for the [TOML](https://toml.io/en/) format.
|
Go library for the [TOML](https://toml.io/) format.
|
||||||
|
|
||||||
|
This library supports TOML version
|
||||||
|
[v1.0.0-rc.3](https://toml.io/en/v1.0.0-rc.3)
|
||||||
|
|
||||||
|
[](https://pkg.go.dev/github.com/pelletier/go-toml)
|
||||||
|
[](https://github.com/pelletier/go-toml/blob/master/LICENSE)
|
||||||
|
[](https://dev.azure.com/pelletierthomas/go-toml-ci/_build/latest?definitionId=1&branchName=master)
|
||||||
|
[](https://codecov.io/gh/pelletier/go-toml)
|
||||||
|
[](https://goreportcard.com/report/github.com/pelletier/go-toml)
|
||||||
|
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fpelletier%2Fgo-toml?ref=badge_shield)
|
||||||
|
|
||||||
This library supports [TOML v1.0.0](https://toml.io/en/v1.0.0).
|
|
||||||
|
|
||||||
## Development status
|
## Development status
|
||||||
|
|
||||||
This is the upcoming major version of go-toml. It is currently in active
|
**ℹ️ Consider go-toml v2!**
|
||||||
development. As of release v2.0.0-beta.1, the library has reached feature parity
|
|
||||||
with v1, and fixes a lot known bugs and performance issues along the way.
|
|
||||||
|
|
||||||
If you do not need the advanced document editing features of v1, you are
|
The next version of go-toml is in [active development][v2-dev], and
|
||||||
encouraged to try out this version.
|
[nearing completion][v2-map].
|
||||||
|
|
||||||
[👉 Roadmap for v2](https://github.com/pelletier/go-toml/discussions/506)
|
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.
|
||||||
|
|
||||||
[🐞 Bug Reports](https://github.com/pelletier/go-toml/issues)
|
The remaining features (Document structure editing and tooling) 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.
|
||||||
|
|
||||||
[💬 Anything else](https://github.com/pelletier/go-toml/discussions)
|
👉 [go-toml v2][v2]
|
||||||
|
|
||||||
## Documentation
|
[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
|
||||||
|
|
||||||
Full API, examples, and implementation notes are available in the Go documentation.
|
## Features
|
||||||
|
|
||||||
[](https://pkg.go.dev/github.com/pelletier/go-toml/v2)
|
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
|
## Import
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "github.com/pelletier/go-toml/v2"
|
import "github.com/pelletier/go-toml"
|
||||||
```
|
```
|
||||||
|
|
||||||
See [Modules](#Modules).
|
## Usage example
|
||||||
|
|
||||||
## Features
|
Read a TOML document:
|
||||||
|
|
||||||
### Stdlib behavior
|
|
||||||
|
|
||||||
As much as possible, this library is designed to behave similarly as the
|
|
||||||
standard library's `encoding/json`.
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
|
|
||||||
While go-toml favors usability, it is written with performance in mind. Most
|
|
||||||
operations should not be shockingly slow. See [benchmarks](#benchmarks).
|
|
||||||
|
|
||||||
### Strict mode
|
|
||||||
|
|
||||||
`Decoder` can be set to "strict mode", which makes it error when some parts of
|
|
||||||
the TOML document was not prevent in the target structure. This is a great way
|
|
||||||
to check for typos. [See example in the documentation][strict].
|
|
||||||
|
|
||||||
[strict]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#example-Decoder.SetStrict
|
|
||||||
|
|
||||||
### Contextualized errors
|
|
||||||
|
|
||||||
When decoding errors occur, go-toml returns [`DecodeError`][decode-err]), which
|
|
||||||
contains a human readable contextualized version of the error. For example:
|
|
||||||
|
|
||||||
```
|
|
||||||
2| key1 = "value1"
|
|
||||||
3| key2 = "missing2"
|
|
||||||
| ~~~~ missing field
|
|
||||||
4| key3 = "missing3"
|
|
||||||
5| key4 = "value4"
|
|
||||||
```
|
|
||||||
|
|
||||||
[decode-err]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#DecodeError
|
|
||||||
|
|
||||||
### Local date and time support
|
|
||||||
|
|
||||||
TOML supports native [local date/times][ldt]. It allows to represent a given
|
|
||||||
date, time, or date-time without relation to a timezone or offset. To support
|
|
||||||
this use-case, go-toml provides [`LocalDate`][tld], [`LocalTime`][tlt], and
|
|
||||||
[`LocalDateTime`][tldt]. Those types can be transformed to and from `time.Time`,
|
|
||||||
making them convenient yet unambiguous structures for their respective TOML
|
|
||||||
representation.
|
|
||||||
|
|
||||||
[ldt]: https://toml.io/en/v1.0.0#local-date-time
|
|
||||||
[tld]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDate
|
|
||||||
[tlt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalTime
|
|
||||||
[tldt]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#LocalDateTime
|
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
|
||||||
Given the following struct, let's see how to read it and write it as TOML:
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type MyConfig struct {
|
config, _ := toml.Load(`
|
||||||
Version int
|
[postgres]
|
||||||
Name string
|
user = "pelletier"
|
||||||
Tags []string
|
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)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Unmarshaling
|
Or use Unmarshal:
|
||||||
|
|
||||||
[`Unmarshal`][unmarshal] reads a TOML document and fills a Go structure with its
|
|
||||||
content. For example:
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
doc := `
|
type Postgres struct {
|
||||||
version = 2
|
User string
|
||||||
name = "go-toml"
|
Password string
|
||||||
tags = ["go", "toml"]
|
}
|
||||||
`
|
type Config struct {
|
||||||
|
Postgres Postgres
|
||||||
var cfg MyConfig
|
|
||||||
err := toml.Unmarshal([]byte(doc), &cfg)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
fmt.Println("version:", cfg.Version)
|
|
||||||
fmt.Println("name:", cfg.Name)
|
|
||||||
fmt.Println("tags:", cfg.Tags)
|
|
||||||
|
|
||||||
// Output:
|
doc := []byte(`
|
||||||
// version: 2
|
[Postgres]
|
||||||
// name: go-toml
|
User = "pelletier"
|
||||||
// tags: [go toml]
|
Password = "mypassword"`)
|
||||||
|
|
||||||
|
config := Config{}
|
||||||
|
toml.Unmarshal(doc, &config)
|
||||||
|
fmt.Println("user=", config.Postgres.User)
|
||||||
```
|
```
|
||||||
|
|
||||||
[unmarshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Unmarshal
|
Or use a query:
|
||||||
|
|
||||||
### Marshaling
|
|
||||||
|
|
||||||
[`Marshal`][marshal] is the opposite of Unmarshal: it represents a Go structure
|
|
||||||
as a TOML document:
|
|
||||||
|
|
||||||
```go
|
```go
|
||||||
cfg := MyConfig{
|
// use a query to gather elements without walking the tree
|
||||||
Version: 2,
|
q, _ := query.Compile("$..[user,password]")
|
||||||
Name: "go-toml",
|
results := q.Execute(config)
|
||||||
Tags: []string{"go", "toml"},
|
for ii, item := range results.Values() {
|
||||||
|
fmt.Printf("Query result %d: %v\n", ii, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := toml.Marshal(cfg)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
fmt.Println(string(b))
|
|
||||||
|
|
||||||
// Output:
|
|
||||||
// Version = 2
|
|
||||||
// Name = 'go-toml'
|
|
||||||
// Tags = ['go', 'toml']
|
|
||||||
```
|
```
|
||||||
|
|
||||||
[marshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Marshal
|
## Documentation
|
||||||
|
|
||||||
## Benchmarks
|
The documentation and additional examples are available at
|
||||||
|
[pkg.go.dev](https://pkg.go.dev/github.com/pelletier/go-toml).
|
||||||
|
|
||||||
Execution time speedup compared to other Go TOML libraries:
|
## Tools
|
||||||
|
|
||||||
<table>
|
Go-toml provides three handy command line tools:
|
||||||
<thead>
|
|
||||||
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr><td>Marshal/HugoFrontMatter-2</td><td>1.9x</td><td>1.9x</td></tr>
|
|
||||||
<tr><td>Marshal/ReferenceFile/map-2</td><td>1.7x</td><td>1.9x</td></tr>
|
|
||||||
<tr><td>Marshal/ReferenceFile/struct-2</td><td>2.4x</td><td>2.6x</td></tr>
|
|
||||||
<tr><td>Unmarshal/HugoFrontMatter-2</td><td>2.9x</td><td>2.5x</td></tr>
|
|
||||||
<tr><td>Unmarshal/ReferenceFile/map-2</td><td>2.7x</td><td>2.6x</td></tr>
|
|
||||||
<tr><td>Unmarshal/ReferenceFile/struct-2</td><td>4.8x</td><td>5.1x</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<details><summary>See more</summary>
|
|
||||||
<p>The table above has the results of the most common use-cases. The table below
|
|
||||||
contains the results of all benchmarks, including unrealistic ones. It is
|
|
||||||
provided for completeness.</p>
|
|
||||||
|
|
||||||
<table>
|
* `tomll`: Reads TOML files and lints them.
|
||||||
<thead>
|
|
||||||
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr><td>Marshal/SimpleDocument/map-2</td><td>1.7x</td><td>2.1x</td></tr>
|
|
||||||
<tr><td>Marshal/SimpleDocument/struct-2</td><td>2.5x</td><td>2.8x</td></tr>
|
|
||||||
<tr><td>Unmarshal/SimpleDocument/map-2</td><td>4.1x</td><td>3.1x</td></tr>
|
|
||||||
<tr><td>Unmarshal/SimpleDocument/struct-2</td><td>6.4x</td><td>4.3x</td></tr>
|
|
||||||
<tr><td>UnmarshalDataset/example-2</td><td>3.4x</td><td>3.2x</td></tr>
|
|
||||||
<tr><td>UnmarshalDataset/code-2</td><td>2.2x</td><td>2.5x</td></tr>
|
|
||||||
<tr><td>UnmarshalDataset/twitter-2</td><td>2.8x</td><td>2.7x</td></tr>
|
|
||||||
<tr><td>UnmarshalDataset/citm_catalog-2</td><td>2.2x</td><td>2.0x</td></tr>
|
|
||||||
<tr><td>UnmarshalDataset/canada-2</td><td>1.8x</td><td>1.4x</td></tr>
|
|
||||||
<tr><td>UnmarshalDataset/config-2</td><td>4.4x</td><td>2.9x</td></tr>
|
|
||||||
<tr><td>[Geo mean]</td><td>2.8x</td><td>2.6x</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Modules
|
```
|
||||||
|
go install github.com/pelletier/go-toml/cmd/tomll
|
||||||
|
tomll --help
|
||||||
|
```
|
||||||
|
* `tomljson`: Reads a TOML file and outputs its JSON representation.
|
||||||
|
|
||||||
go-toml uses Go's standard modules system.
|
```
|
||||||
|
go install github.com/pelletier/go-toml/cmd/tomljson
|
||||||
|
tomljson --help
|
||||||
|
```
|
||||||
|
|
||||||
Installation instructions:
|
* `jsontoml`: Reads a JSON file and outputs a TOML representation.
|
||||||
|
|
||||||
- Go ≥ 1.16: Nothing to do. Use the import in your code. The `go` command deals
|
```
|
||||||
with it automatically.
|
go install github.com/pelletier/go-toml/cmd/jsontoml
|
||||||
- Go ≥ 1.13: `GO111MODULE=on go get github.com/pelletier/go-toml/v2`.
|
jsontoml --help
|
||||||
|
```
|
||||||
|
|
||||||
In case of trouble: [Go Modules FAQ][mod-faq].
|
### Docker image
|
||||||
|
|
||||||
[mod-faq]: https://github.com/golang/go/wiki/Modules#why-does-installing-a-tool-via-go-get-fail-with-error-cannot-find-main-module
|
Those tools are also available as a Docker image from
|
||||||
|
[dockerhub](https://hub.docker.com/r/pelletier/go-toml). For example, to
|
||||||
|
use `tomljson`:
|
||||||
|
|
||||||
## Migrating from v1
|
```
|
||||||
|
docker run -v $PWD:/workdir pelletier/go-toml tomljson /workdir/example.toml
|
||||||
This section describes the differences between v1 and v2, with some pointers on
|
|
||||||
how to get the original behavior when possible.
|
|
||||||
|
|
||||||
### Decoding / Unmarshal
|
|
||||||
|
|
||||||
#### Automatic field name guessing
|
|
||||||
|
|
||||||
When unmarshaling to a struct, if a key in the TOML document does not exactly
|
|
||||||
match the name of a struct field or any of the `toml`-tagged field, v1 tries
|
|
||||||
multiple variations of the key ([code][v1-keys]).
|
|
||||||
|
|
||||||
V2 instead does a case-insensitive matching, like `encoding/json`.
|
|
||||||
|
|
||||||
This could impact you if you are relying on casing to differentiate two fields,
|
|
||||||
and one of them is a not using the `toml` struct tag. The recommended solution
|
|
||||||
is to be specific about tag names for those fields using the `toml` struct tag.
|
|
||||||
|
|
||||||
[v1-keys]: https://github.com/pelletier/go-toml/blob/a2e52561804c6cd9392ebf0048ca64fe4af67a43/marshal.go#L775-L781
|
|
||||||
|
|
||||||
#### Ignore preexisting value in interface
|
|
||||||
|
|
||||||
When decoding into a non-nil `interface{}`, go-toml v1 uses the type of the
|
|
||||||
element in the interface to decode the object. For example:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type inner struct {
|
|
||||||
B interface{}
|
|
||||||
}
|
|
||||||
type doc struct {
|
|
||||||
A interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
d := doc{
|
|
||||||
A: inner{
|
|
||||||
B: "Before",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
data := `
|
|
||||||
[A]
|
|
||||||
B = "After"
|
|
||||||
`
|
|
||||||
|
|
||||||
toml.Unmarshal([]byte(data), &d)
|
|
||||||
fmt.Printf("toml v1: %#v\n", d)
|
|
||||||
|
|
||||||
// toml v1: main.doc{A:main.inner{B:"After"}}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
In this case, field `A` is of type `interface{}`, containing a `inner` struct.
|
Only master (`latest`) and tagged versions are published to dockerhub. You
|
||||||
V1 sees that type and uses it when decoding the object.
|
can build your own image as usual:
|
||||||
|
|
||||||
When decoding an object into an `interface{}`, V2 instead disregards whatever
|
```
|
||||||
value the `interface{}` may contain and replaces it with a
|
docker build -t go-toml .
|
||||||
`map[string]interface{}`. With the same data structure as above, here is what
|
|
||||||
the result looks like:
|
|
||||||
|
|
||||||
```go
|
|
||||||
toml.Unmarshal([]byte(data), &d)
|
|
||||||
fmt.Printf("toml v2: %#v\n", d)
|
|
||||||
|
|
||||||
// toml v2: main.doc{A:map[string]interface {}{"B":"After"}}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This is to match `encoding/json`'s behavior. There is no way to make the v2
|
## Contribute
|
||||||
decoder behave like v1.
|
|
||||||
|
|
||||||
#### Values out of array bounds ignored
|
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!
|
||||||
|
|
||||||
When decoding into an array, v1 returns an error when the number of elements
|
### Run tests
|
||||||
contained in the doc is superior to the capacity of the array. For example:
|
|
||||||
|
|
||||||
```go
|
`go test ./...`
|
||||||
type doc struct {
|
|
||||||
A [2]string
|
|
||||||
}
|
|
||||||
d := doc{}
|
|
||||||
err := toml.Unmarshal([]byte(`A = ["one", "two", "many"]`), &d)
|
|
||||||
fmt.Println(err)
|
|
||||||
|
|
||||||
// (1, 1): unmarshal: TOML array length (3) exceeds destination array length (2)
|
### Fuzzing
|
||||||
```
|
|
||||||
|
|
||||||
In the same situation, v2 ignores the last value:
|
The script `./fuzz.sh` is available to
|
||||||
|
run [go-fuzz](https://github.com/dvyukov/go-fuzz) on go-toml.
|
||||||
|
|
||||||
```go
|
## Versioning
|
||||||
err := toml.Unmarshal([]byte(`A = ["one", "two", "many"]`), &d)
|
|
||||||
fmt.Println("err:", err, "d:", d)
|
|
||||||
// err: <nil> d: {[one two]}
|
|
||||||
```
|
|
||||||
|
|
||||||
This is to match `encoding/json`'s behavior. There is no way to make the v2
|
Go-toml follows [Semantic Versioning](http://semver.org/). The supported version
|
||||||
decoder behave like v1.
|
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
|
||||||
#### Support for `toml.Unmarshaler` has been dropped
|
(see [Go Release Policy](https://golang.org/doc/devel/release.html#policy)).
|
||||||
|
|
||||||
This method was not widely used, poorly defined, and added a lot of complexity.
|
|
||||||
A similar effect can be achieved by implementing the `encoding.TextUnmarshaler`
|
|
||||||
interface and use strings.
|
|
||||||
|
|
||||||
#### Support for `default` struct tag has been dropped
|
|
||||||
|
|
||||||
This feature adds complexity and a poorly defined API for an effect that can be
|
|
||||||
accomplished outside of the library.
|
|
||||||
|
|
||||||
It does not seem like other format parsers in Go support that feature (the
|
|
||||||
project referenced in the original ticket #202 has not been updated since 2017).
|
|
||||||
Given that go-toml v2 should not touch values not in the document, the same
|
|
||||||
effect can be achieved by pre-filling the struct with defaults (libraries like
|
|
||||||
[go-defaults][go-defaults] can help). Also, string representation is not well
|
|
||||||
defined for all types: it creates issues like #278.
|
|
||||||
|
|
||||||
The recommended replacement is pre-filling the struct before unmarshaling.
|
|
||||||
|
|
||||||
[go-defaults]: https://github.com/mcuadros/go-defaults
|
|
||||||
|
|
||||||
### Encoding / Marshal
|
|
||||||
|
|
||||||
#### Default struct fields order
|
|
||||||
|
|
||||||
V1 emits struct fields order alphabetically by default. V2 struct fields are
|
|
||||||
emitted in order they are defined. For example:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type S struct {
|
|
||||||
B string
|
|
||||||
A string
|
|
||||||
}
|
|
||||||
|
|
||||||
data := S{
|
|
||||||
B: "B",
|
|
||||||
A: "A",
|
|
||||||
}
|
|
||||||
|
|
||||||
b, _ := tomlv1.Marshal(data)
|
|
||||||
fmt.Println("v1:\n" + string(b))
|
|
||||||
|
|
||||||
b, _ = tomlv2.Marshal(data)
|
|
||||||
fmt.Println("v2:\n" + string(b))
|
|
||||||
|
|
||||||
// Output:
|
|
||||||
// v1:
|
|
||||||
// A = "A"
|
|
||||||
// B = "B"
|
|
||||||
|
|
||||||
// v2:
|
|
||||||
// B = 'B'
|
|
||||||
// A = 'A'
|
|
||||||
```
|
|
||||||
|
|
||||||
There is no way to make v2 encoder behave like v1. A workaround could be to
|
|
||||||
manually sort the fields alphabetically in the struct definition.
|
|
||||||
|
|
||||||
#### No indentation by default
|
|
||||||
|
|
||||||
V1 automatically indents content of tables by default. V2 does not. However the
|
|
||||||
same behavior can be obtained using [`Encoder.SetIndentTables`][sit]. For example:
|
|
||||||
|
|
||||||
```go
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"table": map[string]string{
|
|
||||||
"key": "value",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
b, _ := tomlv1.Marshal(data)
|
|
||||||
fmt.Println("v1:\n" + string(b))
|
|
||||||
|
|
||||||
b, _ = tomlv2.Marshal(data)
|
|
||||||
fmt.Println("v2:\n" + string(b))
|
|
||||||
|
|
||||||
buf := bytes.Buffer{}
|
|
||||||
enc := tomlv2.NewEncoder(&buf)
|
|
||||||
enc.SetIndentTables(true)
|
|
||||||
enc.Encode(data)
|
|
||||||
fmt.Println("v2 Encoder:\n" + string(buf.Bytes()))
|
|
||||||
|
|
||||||
// Output:
|
|
||||||
// v1:
|
|
||||||
//
|
|
||||||
// [table]
|
|
||||||
// key = "value"
|
|
||||||
//
|
|
||||||
// v2:
|
|
||||||
// [table]
|
|
||||||
// key = 'value'
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// v2 Encoder:
|
|
||||||
// [table]
|
|
||||||
// key = 'value'
|
|
||||||
```
|
|
||||||
|
|
||||||
[sit]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Encoder.SetIndentTables
|
|
||||||
|
|
||||||
#### Keys and strings are single quoted
|
|
||||||
|
|
||||||
V1 always uses double quotes (`"`) around strings and keys that cannot be
|
|
||||||
represented bare (unquoted). V2 uses single quotes instead by default (`'`),
|
|
||||||
unless a character cannot be represented, then falls back to double quotes.
|
|
||||||
|
|
||||||
There is no way to make v2 encoder behave like v1.
|
|
||||||
|
|
||||||
#### `TextMarshaler` emits as a string, not TOML
|
|
||||||
|
|
||||||
Types that implement [`encoding.TextMarshaler`][tm] can emit arbitrary TOML in
|
|
||||||
v1. The encoder would append the result to the output directly. In v2 the result
|
|
||||||
is wrapped in a string. As a result, this interface cannot be implemented by the
|
|
||||||
root object.
|
|
||||||
|
|
||||||
There is no way to make v2 encoder behave like v1.
|
|
||||||
|
|
||||||
[tm]: https://golang.org/pkg/encoding/#TextMarshaler
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
# 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!
|
|
||||||
@@ -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
@@ -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}
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
package benchmark_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"compress/gzip"
|
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/pelletier/go-toml/v2"
|
|
||||||
"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 {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
buf := fixture(t, tc.name)
|
|
||||||
|
|
||||||
var v interface{}
|
|
||||||
require.NoError(t, toml.Unmarshal(buf, &v))
|
|
||||||
|
|
||||||
b, err := json.Marshal(v)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, len(b), tc.jsonLen)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkUnmarshalDataset(b *testing.B) {
|
|
||||||
for _, tc := range bench_inputs {
|
|
||||||
b.Run(tc.name, func(b *testing.B) {
|
|
||||||
buf := fixture(b, tc.name)
|
|
||||||
b.SetBytes(int64(len(buf)))
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
var v interface{}
|
|
||||||
require.NoError(b, toml.Unmarshal(buf, &v))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fixture returns the uncompressed contents of path.
|
|
||||||
func fixture(tb testing.TB, path string) []byte {
|
|
||||||
tb.Helper()
|
|
||||||
|
|
||||||
file := path + ".toml.gz"
|
|
||||||
f, err := os.Open(filepath.Join("testdata", file))
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
tb.Skip("benchmark fixture not found:", file)
|
|
||||||
}
|
|
||||||
require.NoError(tb, err)
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
gz, err := gzip.NewReader(f)
|
|
||||||
require.NoError(tb, err)
|
|
||||||
|
|
||||||
buf, err := ioutil.ReadAll(gz)
|
|
||||||
require.NoError(tb, err)
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -186,7 +186,7 @@ key3 = 1979-05-27T00:32:00.999999-07:00
|
|||||||
key1 = [ 1, 2, 3 ]
|
key1 = [ 1, 2, 3 ]
|
||||||
key2 = [ "red", "yellow", "green" ]
|
key2 = [ "red", "yellow", "green" ]
|
||||||
key3 = [ [ 1, 2 ], [3, 4, 5] ]
|
key3 = [ [ 1, 2 ], [3, 4, 5] ]
|
||||||
key4 = [ [ 1, 2 ], ["a", "b", "c"] ] # this is ok
|
#key4 = [ [ 1, 2 ], ["a", "b", "c"] ] # this is ok
|
||||||
|
|
||||||
# Arrays can also be multiline. So in addition to ignoring whitespace, arrays
|
# Arrays can also be multiline. So in addition to ignoring whitespace, arrays
|
||||||
# also ignore newlines between the brackets. Terminating commas are ok before
|
# also ignore newlines between the brackets. Terminating commas are ok before
|
||||||
|
|||||||
@@ -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: {}
|
||||||
+80
-539
@@ -1,241 +1,17 @@
|
|||||||
package benchmark_test
|
package benchmark
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pelletier/go-toml/v2"
|
burntsushi "github.com/BurntSushi/toml"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/pelletier/go-toml"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUnmarshalSimple(t *testing.T) {
|
|
||||||
doc := []byte(`A = "hello"`)
|
|
||||||
d := struct {
|
|
||||||
A string
|
|
||||||
}{}
|
|
||||||
|
|
||||||
err := toml.Unmarshal(doc, &d)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkUnmarshal(b *testing.B) {
|
|
||||||
b.Run("SimpleDocument", func(b *testing.B) {
|
|
||||||
doc := []byte(`A = "hello"`)
|
|
||||||
|
|
||||||
b.Run("struct", func(b *testing.B) {
|
|
||||||
b.SetBytes(int64(len(doc)))
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
d := struct {
|
|
||||||
A string
|
|
||||||
}{}
|
|
||||||
|
|
||||||
err := toml.Unmarshal(doc, &d)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("map", func(b *testing.B) {
|
|
||||||
b.SetBytes(int64(len(doc)))
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
d := map[string]interface{}{}
|
|
||||||
err := toml.Unmarshal(doc, &d)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("ReferenceFile", func(b *testing.B) {
|
|
||||||
bytes, err := ioutil.ReadFile("benchmark.toml")
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.Run("struct", func(b *testing.B) {
|
|
||||||
b.SetBytes(int64(len(bytes)))
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
d := benchmarkDoc{}
|
|
||||||
err := toml.Unmarshal(bytes, &d)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("map", func(b *testing.B) {
|
|
||||||
b.SetBytes(int64(len(bytes)))
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
d := map[string]interface{}{}
|
|
||||||
err := toml.Unmarshal(bytes, &d)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("HugoFrontMatter", func(b *testing.B) {
|
|
||||||
b.SetBytes(int64(len(hugoFrontMatterbytes)))
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
d := map[string]interface{}{}
|
|
||||||
err := toml.Unmarshal(hugoFrontMatterbytes, &d)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func marshal(v interface{}) ([]byte, error) {
|
|
||||||
var b bytes.Buffer
|
|
||||||
enc := toml.NewEncoder(&b)
|
|
||||||
err := enc.Encode(v)
|
|
||||||
return b.Bytes(), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkMarshal(b *testing.B) {
|
|
||||||
b.Run("SimpleDocument", func(b *testing.B) {
|
|
||||||
doc := []byte(`A = "hello"`)
|
|
||||||
|
|
||||||
b.Run("struct", func(b *testing.B) {
|
|
||||||
d := struct {
|
|
||||||
A string
|
|
||||||
}{}
|
|
||||||
|
|
||||||
err := toml.Unmarshal(doc, &d)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
var out []byte
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
out, err = marshal(d)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.SetBytes(int64(len(out)))
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("map", func(b *testing.B) {
|
|
||||||
d := map[string]interface{}{}
|
|
||||||
err := toml.Unmarshal(doc, &d)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
var out []byte
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
out, err = marshal(d)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.SetBytes(int64(len(out)))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("ReferenceFile", func(b *testing.B) {
|
|
||||||
bytes, err := ioutil.ReadFile("benchmark.toml")
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.Run("struct", func(b *testing.B) {
|
|
||||||
d := benchmarkDoc{}
|
|
||||||
err := toml.Unmarshal(bytes, &d)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
var out []byte
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
out, err = marshal(d)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.SetBytes(int64(len(out)))
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("map", func(b *testing.B) {
|
|
||||||
d := map[string]interface{}{}
|
|
||||||
err := toml.Unmarshal(bytes, &d)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
var out []byte
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
out, err = marshal(d)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.SetBytes(int64(len(out)))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("HugoFrontMatter", func(b *testing.B) {
|
|
||||||
d := map[string]interface{}{}
|
|
||||||
err := toml.Unmarshal(hugoFrontMatterbytes, &d)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
var out []byte
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
out, err = marshal(d)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.SetBytes(int64(len(out)))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type benchmarkDoc struct {
|
type benchmarkDoc struct {
|
||||||
Table struct {
|
Table struct {
|
||||||
Key string
|
Key string
|
||||||
@@ -249,7 +25,7 @@ type benchmarkDoc struct {
|
|||||||
}
|
}
|
||||||
Point struct {
|
Point struct {
|
||||||
X int64
|
X int64
|
||||||
Y int64
|
U int64
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,7 +97,7 @@ type benchmarkDoc struct {
|
|||||||
Key1 []int64
|
Key1 []int64
|
||||||
Key2 []string
|
Key2 []string
|
||||||
Key3 [][]int64
|
Key3 [][]int64
|
||||||
Key4 []interface{}
|
// TODO: Key4 not supported by go-toml's Unmarshal
|
||||||
Key5 []int64
|
Key5 []int64
|
||||||
Key6 []int64
|
Key6 []int64
|
||||||
}
|
}
|
||||||
@@ -333,321 +109,86 @@ type benchmarkDoc struct {
|
|||||||
Fruit []struct {
|
Fruit []struct {
|
||||||
Name string
|
Name string
|
||||||
Physical struct {
|
Physical struct {
|
||||||
Color string
|
Color string
|
||||||
Shape string
|
Shape string
|
||||||
}
|
Variety []struct {
|
||||||
Variety []struct {
|
Name string
|
||||||
Name string
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnmarshalReferenceFile(t *testing.T) {
|
func BenchmarkParseToml(b *testing.B) {
|
||||||
|
fileBytes, err := ioutil.ReadFile("benchmark.toml")
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := toml.LoadReader(bytes.NewReader(fileBytes))
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkUnmarshalToml(b *testing.B) {
|
||||||
bytes, err := ioutil.ReadFile("benchmark.toml")
|
bytes, err := ioutil.ReadFile("benchmark.toml")
|
||||||
require.NoError(t, err)
|
if err != nil {
|
||||||
d := benchmarkDoc{}
|
b.Fatal(err)
|
||||||
err = toml.Unmarshal(bytes, &d)
|
}
|
||||||
require.NoError(t, err)
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
expected := benchmarkDoc{
|
for i := 0; i < b.N; i++ {
|
||||||
Table: struct {
|
target := benchmarkDoc{}
|
||||||
Key string
|
err := toml.Unmarshal(bytes, &target)
|
||||||
Subtable struct{ Key string }
|
if err != nil {
|
||||||
Inline struct {
|
b.Fatal(err)
|
||||||
Name struct {
|
}
|
||||||
First string
|
|
||||||
Last string
|
|
||||||
}
|
|
||||||
Point struct {
|
|
||||||
X int64
|
|
||||||
Y int64
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}{
|
|
||||||
Key: "value",
|
|
||||||
Subtable: struct{ Key string }{
|
|
||||||
Key: "another value",
|
|
||||||
},
|
|
||||||
// note: x.y.z.w is purposefully missing
|
|
||||||
Inline: struct {
|
|
||||||
Name struct {
|
|
||||||
First string
|
|
||||||
Last string
|
|
||||||
}
|
|
||||||
Point struct {
|
|
||||||
X int64
|
|
||||||
Y int64
|
|
||||||
}
|
|
||||||
}{
|
|
||||||
Name: struct {
|
|
||||||
First string
|
|
||||||
Last string
|
|
||||||
}{
|
|
||||||
First: "Tom",
|
|
||||||
Last: "Preston-Werner",
|
|
||||||
},
|
|
||||||
Point: struct {
|
|
||||||
X int64
|
|
||||||
Y int64
|
|
||||||
}{
|
|
||||||
X: 1,
|
|
||||||
Y: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
String: struct {
|
|
||||||
Basic struct{ Basic string }
|
|
||||||
Multiline struct {
|
|
||||||
Key1 string
|
|
||||||
Key2 string
|
|
||||||
Key3 string
|
|
||||||
Continued struct {
|
|
||||||
Key1 string
|
|
||||||
Key2 string
|
|
||||||
Key3 string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Literal struct {
|
|
||||||
Winpath string
|
|
||||||
Winpath2 string
|
|
||||||
Quoted string
|
|
||||||
Regex string
|
|
||||||
Multiline struct {
|
|
||||||
Regex2 string
|
|
||||||
Lines string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}{
|
|
||||||
Basic: struct{ Basic string }{
|
|
||||||
Basic: "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF.",
|
|
||||||
},
|
|
||||||
Multiline: struct {
|
|
||||||
Key1 string
|
|
||||||
Key2 string
|
|
||||||
Key3 string
|
|
||||||
Continued struct {
|
|
||||||
Key1 string
|
|
||||||
Key2 string
|
|
||||||
Key3 string
|
|
||||||
}
|
|
||||||
}{
|
|
||||||
Key1: "One\nTwo",
|
|
||||||
Key2: "One\nTwo",
|
|
||||||
Key3: "One\nTwo",
|
|
||||||
|
|
||||||
Continued: struct {
|
|
||||||
Key1 string
|
|
||||||
Key2 string
|
|
||||||
Key3 string
|
|
||||||
}{
|
|
||||||
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.`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Literal: struct {
|
|
||||||
Winpath string
|
|
||||||
Winpath2 string
|
|
||||||
Quoted string
|
|
||||||
Regex string
|
|
||||||
Multiline struct {
|
|
||||||
Regex2 string
|
|
||||||
Lines string
|
|
||||||
}
|
|
||||||
}{
|
|
||||||
Winpath: `C:\Users\nodejs\templates`,
|
|
||||||
Winpath2: `\\ServerX\admin$\system32\`,
|
|
||||||
Quoted: `Tom "Dubs" Preston-Werner`,
|
|
||||||
Regex: `<\i\c*\s*>`,
|
|
||||||
|
|
||||||
Multiline: struct {
|
|
||||||
Regex2 string
|
|
||||||
Lines string
|
|
||||||
}{
|
|
||||||
Regex2: `I [dw]on't need \d{2} apples`,
|
|
||||||
Lines: `The first newline is
|
|
||||||
trimmed in raw strings.
|
|
||||||
All other whitespace
|
|
||||||
is preserved.
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Integer: struct {
|
|
||||||
Key1 int64
|
|
||||||
Key2 int64
|
|
||||||
Key3 int64
|
|
||||||
Key4 int64
|
|
||||||
Underscores struct {
|
|
||||||
Key1 int64
|
|
||||||
Key2 int64
|
|
||||||
Key3 int64
|
|
||||||
}
|
|
||||||
}{
|
|
||||||
Key1: 99,
|
|
||||||
Key2: 42,
|
|
||||||
Key3: 0,
|
|
||||||
Key4: -17,
|
|
||||||
|
|
||||||
Underscores: struct {
|
|
||||||
Key1 int64
|
|
||||||
Key2 int64
|
|
||||||
Key3 int64
|
|
||||||
}{
|
|
||||||
Key1: 1000,
|
|
||||||
Key2: 5349221,
|
|
||||||
Key3: 12345,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Float: struct {
|
|
||||||
Fractional struct {
|
|
||||||
Key1 float64
|
|
||||||
Key2 float64
|
|
||||||
Key3 float64
|
|
||||||
}
|
|
||||||
Exponent struct {
|
|
||||||
Key1 float64
|
|
||||||
Key2 float64
|
|
||||||
Key3 float64
|
|
||||||
}
|
|
||||||
Both struct{ Key float64 }
|
|
||||||
Underscores struct {
|
|
||||||
Key1 float64
|
|
||||||
Key2 float64
|
|
||||||
}
|
|
||||||
}{
|
|
||||||
Fractional: struct {
|
|
||||||
Key1 float64
|
|
||||||
Key2 float64
|
|
||||||
Key3 float64
|
|
||||||
}{
|
|
||||||
Key1: 1.0,
|
|
||||||
Key2: 3.1415,
|
|
||||||
Key3: -0.01,
|
|
||||||
},
|
|
||||||
Exponent: struct {
|
|
||||||
Key1 float64
|
|
||||||
Key2 float64
|
|
||||||
Key3 float64
|
|
||||||
}{
|
|
||||||
Key1: 5e+22,
|
|
||||||
Key2: 1e6,
|
|
||||||
Key3: -2e-2,
|
|
||||||
},
|
|
||||||
Both: struct{ Key float64 }{
|
|
||||||
Key: 6.626e-34,
|
|
||||||
},
|
|
||||||
Underscores: struct {
|
|
||||||
Key1 float64
|
|
||||||
Key2 float64
|
|
||||||
}{
|
|
||||||
Key1: 9224617.445991228313,
|
|
||||||
Key2: 1e100,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Boolean: struct {
|
|
||||||
True bool
|
|
||||||
False bool
|
|
||||||
}{
|
|
||||||
True: true,
|
|
||||||
False: false,
|
|
||||||
},
|
|
||||||
Datetime: struct {
|
|
||||||
Key1 time.Time
|
|
||||||
Key2 time.Time
|
|
||||||
Key3 time.Time
|
|
||||||
}{
|
|
||||||
Key1: time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC),
|
|
||||||
Key2: time.Date(1979, 5, 27, 0, 32, 0, 0, time.FixedZone("", -7*3600)),
|
|
||||||
Key3: time.Date(1979, 5, 27, 0, 32, 0, 999999000, time.FixedZone("", -7*3600)),
|
|
||||||
},
|
|
||||||
Array: struct {
|
|
||||||
Key1 []int64
|
|
||||||
Key2 []string
|
|
||||||
Key3 [][]int64
|
|
||||||
Key4 []interface{}
|
|
||||||
Key5 []int64
|
|
||||||
Key6 []int64
|
|
||||||
}{
|
|
||||||
Key1: []int64{1, 2, 3},
|
|
||||||
Key2: []string{"red", "yellow", "green"},
|
|
||||||
Key3: [][]int64{{1, 2}, {3, 4, 5}},
|
|
||||||
Key4: []interface{}{
|
|
||||||
[]interface{}{int64(1), int64(2)},
|
|
||||||
[]interface{}{"a", "b", "c"},
|
|
||||||
},
|
|
||||||
Key5: []int64{1, 2, 3},
|
|
||||||
Key6: []int64{1, 2},
|
|
||||||
},
|
|
||||||
Products: []struct {
|
|
||||||
Name string
|
|
||||||
Sku int64
|
|
||||||
Color string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "Hammer",
|
|
||||||
Sku: 738594937,
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
Name: "Nail",
|
|
||||||
Sku: 284758393,
|
|
||||||
Color: "gray",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Fruit: []struct {
|
|
||||||
Name string
|
|
||||||
Physical struct {
|
|
||||||
Color string
|
|
||||||
Shape string
|
|
||||||
}
|
|
||||||
Variety []struct{ Name string }
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "apple",
|
|
||||||
Physical: struct {
|
|
||||||
Color string
|
|
||||||
Shape string
|
|
||||||
}{
|
|
||||||
Color: "red",
|
|
||||||
Shape: "round",
|
|
||||||
},
|
|
||||||
Variety: []struct{ Name string }{
|
|
||||||
{Name: "red delicious"},
|
|
||||||
{Name: "granny smith"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "banana",
|
|
||||||
Variety: []struct{ Name string }{
|
|
||||||
{Name: "plantain"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
require.Equal(t, expected, d)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var hugoFrontMatterbytes = []byte(`
|
func BenchmarkUnmarshalBurntSushiToml(b *testing.B) {
|
||||||
categories = ["Development", "VIM"]
|
bytes, err := ioutil.ReadFile("benchmark.toml")
|
||||||
date = "2012-04-06"
|
if err != nil {
|
||||||
description = "spf13-vim is a cross platform distribution of vim plugins and resources for Vim."
|
b.Fatal(err)
|
||||||
slug = "spf13-vim-3-0-release-and-new-website"
|
}
|
||||||
tags = [".vimrc", "plugins", "spf13-vim", "vim"]
|
b.ResetTimer()
|
||||||
title = "spf13-vim 3.0 release and new website"
|
for i := 0; i < b.N; i++ {
|
||||||
include_toc = true
|
target := benchmarkDoc{}
|
||||||
show_comments = false
|
err := burntsushi.Unmarshal(bytes, &target)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[[cascade]]
|
func BenchmarkUnmarshalJson(b *testing.B) {
|
||||||
background = "yosemite.jpg"
|
bytes, err := ioutil.ReadFile("benchmark.json")
|
||||||
[cascade._target]
|
if err != nil {
|
||||||
kind = "page"
|
b.Fatal(err)
|
||||||
lang = "en"
|
}
|
||||||
path = "/blog/**"
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
target := benchmarkDoc{}
|
||||||
|
err := json.Unmarshal(bytes, &target)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[[cascade]]
|
func BenchmarkUnmarshalYaml(b *testing.B) {
|
||||||
background = "goldenbridge.jpg"
|
bytes, err := ioutil.ReadFile("benchmark.yml")
|
||||||
[cascade._target]
|
if err != nil {
|
||||||
kind = "section"
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
module github.com/pelletier/go-toml/benchmark
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/toml v0.3.1
|
||||||
|
github.com/pelletier/go-toml v0.0.0
|
||||||
|
gopkg.in/yaml.v2 v2.3.0
|
||||||
|
)
|
||||||
|
|
||||||
|
replace github.com/pelletier/go-toml => ../
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -1,71 +0,0 @@
|
|||||||
package toml
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var valid10Ascii = []byte("1234567890")
|
|
||||||
var valid10Utf8 = []byte("日本語a")
|
|
||||||
var valid1kUtf8 = bytes.Repeat([]byte("0123456789日本語日本語日本語日abcdefghijklmnopqrstuvwx"), 16)
|
|
||||||
var valid1MUtf8 = bytes.Repeat(valid1kUtf8, 1024)
|
|
||||||
var valid1kAscii = bytes.Repeat([]byte("012345678998jhjklasDJKLAAdjdfjsdklfjdslkabcdefghijklmnopqrstuvwx"), 16)
|
|
||||||
var valid1MAscii = bytes.Repeat(valid1kAscii, 1024)
|
|
||||||
|
|
||||||
func BenchmarkScanComments(b *testing.B) {
|
|
||||||
wrap := func(x []byte) []byte {
|
|
||||||
return []byte("# " + string(x) + "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
inputs := map[string][]byte{
|
|
||||||
"10Valid": wrap(valid10Ascii),
|
|
||||||
"1kValid": wrap(valid1kAscii),
|
|
||||||
"1MValid": wrap(valid1MAscii),
|
|
||||||
"10ValidUtf8": wrap(valid10Utf8),
|
|
||||||
"1kValidUtf8": wrap(valid1kUtf8),
|
|
||||||
"1MValidUtf8": wrap(valid1MUtf8),
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, input := range inputs {
|
|
||||||
b.Run(name, func(b *testing.B) {
|
|
||||||
b.SetBytes(int64(len(input)))
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
scanComment(input)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkParseLiteralStringValid(b *testing.B) {
|
|
||||||
wrap := func(x []byte) []byte {
|
|
||||||
return []byte("'" + string(x) + "'")
|
|
||||||
}
|
|
||||||
|
|
||||||
inputs := map[string][]byte{
|
|
||||||
"10Valid": wrap(valid10Ascii),
|
|
||||||
"1kValid": wrap(valid1kAscii),
|
|
||||||
"1MValid": wrap(valid1MAscii),
|
|
||||||
"10ValidUtf8": wrap(valid10Utf8),
|
|
||||||
"1kValidUtf8": wrap(valid1kUtf8),
|
|
||||||
"1MValidUtf8": wrap(valid1MUtf8),
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, input := range inputs {
|
|
||||||
b.Run(name, func(b *testing.B) {
|
|
||||||
p := parser{}
|
|
||||||
b.SetBytes(int64(len(input)))
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, _, _, err := p.parseLiteralString(input)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,271 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
|
|
||||||
stderr() {
|
|
||||||
echo "$@" 1>&2
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
b=$(basename "$0")
|
|
||||||
echo $b: ERROR: "$@" 1>&2
|
|
||||||
|
|
||||||
cat 1>&2 <<EOF
|
|
||||||
|
|
||||||
DESCRIPTION
|
|
||||||
|
|
||||||
$(basename "$0") is the script to run continuous integration commands for
|
|
||||||
go-toml on unix.
|
|
||||||
|
|
||||||
Requires Go and Git to be available in the PATH. Expects to be ran from the
|
|
||||||
root of go-toml's Git repository.
|
|
||||||
|
|
||||||
USAGE
|
|
||||||
|
|
||||||
$b COMMAND [OPTIONS...]
|
|
||||||
|
|
||||||
COMMANDS
|
|
||||||
|
|
||||||
benchmark [OPTIONS...] [BRANCH]
|
|
||||||
|
|
||||||
Run benchmarks.
|
|
||||||
|
|
||||||
ARGUMENTS
|
|
||||||
|
|
||||||
BRANCH Optional. Defines which Git branch to use when running
|
|
||||||
benchmarks.
|
|
||||||
|
|
||||||
OPTIONS
|
|
||||||
|
|
||||||
-d Compare benchmarks of HEAD with BRANCH using benchstats. In
|
|
||||||
this form the BRANCH argument is required.
|
|
||||||
|
|
||||||
-a Compare benchmarks of HEAD against go-toml v1 and
|
|
||||||
BurntSushi/toml.
|
|
||||||
|
|
||||||
-html When used with -a, emits the output as HTML, ready to be
|
|
||||||
embedded in the README.
|
|
||||||
|
|
||||||
coverage [OPTIONS...] [BRANCH]
|
|
||||||
|
|
||||||
Generates code coverage.
|
|
||||||
|
|
||||||
ARGUMENTS
|
|
||||||
|
|
||||||
BRANCH Optional. Defines which Git branch to use when reporting
|
|
||||||
coverage. Defaults to HEAD.
|
|
||||||
|
|
||||||
OPTIONS
|
|
||||||
|
|
||||||
-d Compare coverage of HEAD with the one of BRANCH. In this form,
|
|
||||||
the BRANCH argument is required. Exit code is non-zero when
|
|
||||||
coverage percentage decreased.
|
|
||||||
EOF
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
cover() {
|
|
||||||
branch="${1}"
|
|
||||||
dir="$(mktemp -d)"
|
|
||||||
|
|
||||||
stderr "Executing coverage for ${branch} at ${dir}"
|
|
||||||
|
|
||||||
if [ "${branch}" = "HEAD" ]; then
|
|
||||||
cp -r . "${dir}/"
|
|
||||||
else
|
|
||||||
git worktree add "$dir" "$branch"
|
|
||||||
fi
|
|
||||||
|
|
||||||
pushd "$dir"
|
|
||||||
go test -covermode=atomic -coverprofile=coverage.out ./...
|
|
||||||
go tool cover -func=coverage.out
|
|
||||||
popd
|
|
||||||
|
|
||||||
if [ "${branch}" != "HEAD" ]; then
|
|
||||||
git worktree remove --force "$dir"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
coverage() {
|
|
||||||
case "$1" in
|
|
||||||
-d)
|
|
||||||
shift
|
|
||||||
target="${1?Need to provide a target branch argument}"
|
|
||||||
|
|
||||||
output_dir="$(mktemp -d)"
|
|
||||||
target_out="${output_dir}/target.txt"
|
|
||||||
head_out="${output_dir}/head.txt"
|
|
||||||
|
|
||||||
cover "${target}" > "${target_out}"
|
|
||||||
cover "HEAD" > "${head_out}"
|
|
||||||
|
|
||||||
cat "${target_out}"
|
|
||||||
cat "${head_out}"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
target_pct="$(cat ${target_out} |sed -E 's/.*total.*\t([0-9.]+)%/\1/;t;d')"
|
|
||||||
head_pct="$(cat ${head_out} |sed -E 's/.*total.*\t([0-9.]+)%/\1/;t;d')"
|
|
||||||
echo "Results: ${target} ${target_pct}% HEAD ${head_pct}%"
|
|
||||||
|
|
||||||
delta_pct=$(echo "$head_pct - $target_pct" | bc -l)
|
|
||||||
echo "Delta: ${delta_pct}"
|
|
||||||
|
|
||||||
if [[ $delta_pct = \-* ]]; then
|
|
||||||
echo "Regression!";
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
cover "${1-HEAD}"
|
|
||||||
}
|
|
||||||
|
|
||||||
bench() {
|
|
||||||
branch="${1}"
|
|
||||||
out="${2}"
|
|
||||||
replace="${3}"
|
|
||||||
dir="$(mktemp -d)"
|
|
||||||
|
|
||||||
stderr "Executing benchmark for ${branch} at ${dir}"
|
|
||||||
|
|
||||||
if [ "${branch}" = "HEAD" ]; then
|
|
||||||
cp -r . "${dir}/"
|
|
||||||
else
|
|
||||||
git worktree add "$dir" "$branch"
|
|
||||||
fi
|
|
||||||
|
|
||||||
pushd "$dir"
|
|
||||||
|
|
||||||
if [ "${replace}" != "" ]; then
|
|
||||||
find ./benchmark/ -iname '*.go' -exec sed -i -E "s|github.com/pelletier/go-toml/v2|${replace}|g" {} \;
|
|
||||||
go get "${replace}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
export GOMAXPROCS=2
|
|
||||||
nice -n -19 taskset --cpu-list 0,1 go test '-bench=^Benchmark(Un)?[mM]arshal' -count=5 -run=Nothing ./... | tee "${out}"
|
|
||||||
popd
|
|
||||||
|
|
||||||
if [ "${branch}" != "HEAD" ]; then
|
|
||||||
git worktree remove --force "$dir"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
fmktemp() {
|
|
||||||
if mktemp --version|grep GNU >/dev/null; then
|
|
||||||
mktemp --suffix=-$1;
|
|
||||||
else
|
|
||||||
mktemp -t $1;
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
benchstathtml() {
|
|
||||||
python3 - $1 <<'EOF'
|
|
||||||
import sys
|
|
||||||
|
|
||||||
lines = []
|
|
||||||
stop = False
|
|
||||||
|
|
||||||
with open(sys.argv[1]) as f:
|
|
||||||
for line in f.readlines():
|
|
||||||
line = line.strip()
|
|
||||||
if line == "":
|
|
||||||
stop = True
|
|
||||||
if not stop:
|
|
||||||
lines.append(line.split(','))
|
|
||||||
|
|
||||||
results = []
|
|
||||||
for line in reversed(lines[1:]):
|
|
||||||
v2 = float(line[1])
|
|
||||||
results.append([
|
|
||||||
line[0].replace("-32", ""),
|
|
||||||
"%.1fx" % (float(line[3])/v2), # v1
|
|
||||||
"%.1fx" % (float(line[5])/v2), # bs
|
|
||||||
])
|
|
||||||
# move geomean to the end
|
|
||||||
results.append(results[0])
|
|
||||||
del results[0]
|
|
||||||
|
|
||||||
|
|
||||||
def printtable(data):
|
|
||||||
print("""
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr><th>Benchmark</th><th>go-toml v1</th><th>BurntSushi/toml</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>""")
|
|
||||||
|
|
||||||
for r in data:
|
|
||||||
print(" <tr><td>{}</td><td>{}</td><td>{}</td></tr>".format(*r))
|
|
||||||
|
|
||||||
print(""" </tbody>
|
|
||||||
</table>""")
|
|
||||||
|
|
||||||
|
|
||||||
def match(x):
|
|
||||||
return "ReferenceFile" in x[0] or "HugoFrontMatter" in x[0]
|
|
||||||
|
|
||||||
above = [x for x in results if match(x)]
|
|
||||||
below = [x for x in results if not match(x)]
|
|
||||||
|
|
||||||
printtable(above)
|
|
||||||
print("<details><summary>See more</summary>")
|
|
||||||
print("""<p>The table above has the results of the most common use-cases. The table below
|
|
||||||
contains the results of all benchmarks, including unrealistic ones. It is
|
|
||||||
provided for completeness.</p>""")
|
|
||||||
printtable(below)
|
|
||||||
print('<p>This table can be generated with <code>./ci.sh benchmark -a -html</code>.</p>')
|
|
||||||
print("</details>")
|
|
||||||
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
benchmark() {
|
|
||||||
case "$1" in
|
|
||||||
-d)
|
|
||||||
shift
|
|
||||||
target="${1?Need to provide a target branch argument}"
|
|
||||||
|
|
||||||
old=`fmktemp ${target}`
|
|
||||||
bench "${target}" "${old}"
|
|
||||||
|
|
||||||
new=`fmktemp HEAD`
|
|
||||||
bench HEAD "${new}"
|
|
||||||
|
|
||||||
benchstat "${old}" "${new}"
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
-a)
|
|
||||||
shift
|
|
||||||
|
|
||||||
v2stats=`fmktemp go-toml-v2`
|
|
||||||
bench HEAD "${v2stats}" "github.com/pelletier/go-toml/v2"
|
|
||||||
v1stats=`fmktemp go-toml-v1`
|
|
||||||
bench HEAD "${v1stats}" "github.com/pelletier/go-toml"
|
|
||||||
bsstats=`fmktemp bs-toml`
|
|
||||||
bench HEAD "${bsstats}" "github.com/BurntSushi/toml"
|
|
||||||
|
|
||||||
cp "${v2stats}" go-toml-v2.txt
|
|
||||||
cp "${v1stats}" go-toml-v1.txt
|
|
||||||
cp "${bsstats}" bs-toml.txt
|
|
||||||
|
|
||||||
if [ "$1" = "-html" ]; then
|
|
||||||
tmpcsv=`fmktemp csv`
|
|
||||||
benchstat -csv -geomean go-toml-v2.txt go-toml-v1.txt bs-toml.txt > $tmpcsv
|
|
||||||
benchstathtml $tmpcsv
|
|
||||||
else
|
|
||||||
benchstat -geomean go-toml-v2.txt go-toml-v1.txt bs-toml.txt
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f go-toml-v2.txt go-toml-v1.txt bs-toml.txt
|
|
||||||
return $?
|
|
||||||
esac
|
|
||||||
|
|
||||||
bench "${1-HEAD}" `mktemp`
|
|
||||||
}
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
coverage) shift; coverage $@;;
|
|
||||||
benchmark) shift; benchmark $@;;
|
|
||||||
*) usage "bad argument $1";;
|
|
||||||
esac
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/pelletier/go-toml/v2/testsuite"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log.SetFlags(0)
|
|
||||||
flag.Usage = usage
|
|
||||||
flag.Parse()
|
|
||||||
if flag.NArg() != 0 {
|
|
||||||
flag.Usage()
|
|
||||||
}
|
|
||||||
|
|
||||||
err := testsuite.DecodeStdin()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func usage() {
|
|
||||||
log.Printf("Usage: %s < toml-file\n", path.Base(os.Args[0]))
|
|
||||||
flag.PrintDefaults()
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// 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 (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
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.")
|
||||||
|
}
|
||||||
|
flag.Parse()
|
||||||
|
// read from stdin and print to stdout
|
||||||
|
if flag.NArg() == 0 {
|
||||||
|
s, err := lintReader(os.Stdin)
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
io.WriteString(os.Stderr, err.Error())
|
||||||
|
os.Exit(-1)
|
||||||
|
}
|
||||||
|
ioutil.WriteFile(filename, []byte(s), 0644)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lintFile(filename string) (string, error) {
|
||||||
|
tree, err := toml.LoadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return tree.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func lintReader(r io.Reader) (string, error) {
|
||||||
|
tree, err := toml.LoadReader(r)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return tree.String(), nil
|
||||||
|
}
|
||||||
+19
-23
@@ -1,9 +1,8 @@
|
|||||||
// tomltestgen retrieves a given version of the language-agnostic TOML test suite in
|
// Tomltestgen is a program that retrieves a given version of
|
||||||
// https://github.com/BurntSushi/toml-test and generates go-toml unit tests.
|
// https://github.com/BurntSushi/toml-test and generates go code for go-toml's unit tests
|
||||||
|
// based on the test files.
|
||||||
//
|
//
|
||||||
// Within the go-toml package, run `go generate`. Otherwise, use:
|
// Usage: go run github.com/pelletier/go-toml/cmd/tomltestgen > toml_testgen_test.go
|
||||||
//
|
|
||||||
// go run github.com/pelletier/go-toml/cmd/tomltestgen -o toml_testgen_test.go
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -44,20 +43,20 @@ type testsCollection struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const srcTemplate = "// Generated by tomltestgen for toml-test ref {{.Ref}} on {{.Timestamp}}\n" +
|
const srcTemplate = "// Generated by tomltestgen for toml-test ref {{.Ref}} on {{.Timestamp}}\n" +
|
||||||
"package toml_test\n" +
|
"package toml\n" +
|
||||||
" import (\n" +
|
" import (\n" +
|
||||||
" \"testing\"\n" +
|
" \"testing\"\n" +
|
||||||
")\n" +
|
")\n" +
|
||||||
|
|
||||||
"{{range .Invalid}}\n" +
|
"{{range .Invalid}}\n" +
|
||||||
"func TestTOMLTest_Invalid_{{.Name}}(t *testing.T) {\n" +
|
"func TestInvalid{{.Name}}(t *testing.T) {\n" +
|
||||||
" input := {{.Input|gostr}}\n" +
|
" input := {{.Input|gostr}}\n" +
|
||||||
" testgenInvalid(t, input)\n" +
|
" testgenInvalid(t, input)\n" +
|
||||||
"}\n" +
|
"}\n" +
|
||||||
"{{end}}\n" +
|
"{{end}}\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"{{range .Valid}}\n" +
|
"{{range .Valid}}\n" +
|
||||||
"func TestTOMLTest_Valid_{{.Name}}(t *testing.T) {\n" +
|
"func TestValid{{.Name}}(t *testing.T) {\n" +
|
||||||
" input := {{.Input|gostr}}\n" +
|
" input := {{.Input|gostr}}\n" +
|
||||||
" jsonRef := {{.JsonRef|gostr}}\n" +
|
" jsonRef := {{.JsonRef|gostr}}\n" +
|
||||||
" testgenValid(t, input, jsonRef)\n" +
|
" testgenValid(t, input, jsonRef)\n" +
|
||||||
@@ -97,9 +96,6 @@ func kebabToCamel(kebab string) string {
|
|||||||
nextUpper = false
|
nextUpper = false
|
||||||
} else if c == '-' {
|
} else if c == '-' {
|
||||||
nextUpper = true
|
nextUpper = true
|
||||||
} else if c == '/' {
|
|
||||||
nextUpper = true
|
|
||||||
camel += "_"
|
|
||||||
} else {
|
} else {
|
||||||
camel += string(c)
|
camel += string(c)
|
||||||
}
|
}
|
||||||
@@ -121,12 +117,21 @@ func readFileFromZip(f *zip.File) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func templateGoStr(input string) string {
|
func templateGoStr(input string) string {
|
||||||
return strconv.Quote(input)
|
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 (
|
var (
|
||||||
ref = flag.String("r", "master", "git reference")
|
ref = flag.String("r", "master", "git reference")
|
||||||
out = flag.String("o", "", "output file")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func usage() {
|
func usage() {
|
||||||
@@ -210,14 +215,5 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
fmt.Println(string(outputBytes))
|
||||||
if *out == "" {
|
|
||||||
fmt.Println(string(outputBytes))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(*out, outputBytes, 0644)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,496 +0,0 @@
|
|||||||
package toml
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"strconv"
|
|
||||||
"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:
|
|
||||||
panic(fmt.Errorf("invalid base '%c', should have been checked by scanIntOrFloat", 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 LocalDate{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
date.Month, err = parseDecimalDigits(b[5:7])
|
|
||||||
if err != nil {
|
|
||||||
return LocalDate{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
date.Day, err = parseDecimalDigits(b[8:10])
|
|
||||||
if err != nil {
|
|
||||||
return LocalDate{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isValidDate(date.Year, date.Month, date.Day) {
|
|
||||||
return LocalDate{}, newDecodeError(b, "impossible date")
|
|
||||||
}
|
|
||||||
|
|
||||||
return date, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseDecimalDigits(b []byte) (int, error) {
|
|
||||||
v := 0
|
|
||||||
|
|
||||||
for i, c := range b {
|
|
||||||
if c < '0' || c > '9' {
|
|
||||||
return 0, newDecodeError(b[i:i+1], "expected digit (0-9)")
|
|
||||||
}
|
|
||||||
v *= 10
|
|
||||||
v += int(c - '0')
|
|
||||||
}
|
|
||||||
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
// parser should have checked that when assigning the date time node
|
|
||||||
panic("date time should have a timezone")
|
|
||||||
}
|
|
||||||
|
|
||||||
if b[0] == 'Z' || 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
|
|
||||||
if b[0] == '-' {
|
|
||||||
direction = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
hours := digitsToInt(b[1:3])
|
|
||||||
minutes := digitsToInt(b[4:6])
|
|
||||||
seconds := direction * (hours*3600 + minutes*60)
|
|
||||||
zone = time.FixedZone("", seconds)
|
|
||||||
b = b[dateTimeByteLen:]
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(b) > 0 {
|
|
||||||
return time.Time{}, newDecodeError(b, "extra bytes at the end of the timezone")
|
|
||||||
}
|
|
||||||
|
|
||||||
t := time.Date(
|
|
||||||
dt.Year,
|
|
||||||
time.Month(dt.Month),
|
|
||||||
dt.Day,
|
|
||||||
dt.Hour,
|
|
||||||
dt.Minute,
|
|
||||||
dt.Second,
|
|
||||||
dt.Nanosecond,
|
|
||||||
zone)
|
|
||||||
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseLocalDateTime(b []byte) (LocalDateTime, []byte, error) {
|
|
||||||
var dt LocalDateTime
|
|
||||||
|
|
||||||
const localDateTimeByteMinLen = 11
|
|
||||||
if len(b) < localDateTimeByteMinLen {
|
|
||||||
return dt, nil, newDecodeError(b, "local datetimes are expected to have the format YYYY-MM-DDTHH:MM:SS[.NNNNNNNNN]")
|
|
||||||
}
|
|
||||||
|
|
||||||
date, err := parseLocalDate(b[:10])
|
|
||||||
if err != nil {
|
|
||||||
return dt, nil, err
|
|
||||||
}
|
|
||||||
dt.LocalDate = date
|
|
||||||
|
|
||||||
sep := b[10]
|
|
||||||
if sep != 'T' && sep != ' ' && sep != 't' {
|
|
||||||
return dt, nil, newDecodeError(b[10:11], "datetime separator is expected to be T or a space")
|
|
||||||
}
|
|
||||||
|
|
||||||
t, rest, err := parseLocalTime(b[11:])
|
|
||||||
if err != nil {
|
|
||||||
return dt, nil, err
|
|
||||||
}
|
|
||||||
dt.LocalTime = t
|
|
||||||
|
|
||||||
return dt, rest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (
|
|
||||||
nspow = [10]int{0, 1e8, 1e7, 1e6, 1e5, 1e4, 1e3, 1e2, 1e1, 1e0}
|
|
||||||
t LocalTime
|
|
||||||
)
|
|
||||||
|
|
||||||
// check if b matches to have expected format HH:MM:SS[.NNNNNN]
|
|
||||||
const localTimeByteLen = 8
|
|
||||||
if len(b) < localTimeByteLen {
|
|
||||||
return t, nil, newDecodeError(b, "times are expected to have the format HH:MM:SS[.NNNNNN]")
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
t.Hour, err = parseDecimalDigits(b[0:2])
|
|
||||||
if err != nil {
|
|
||||||
return t, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.Hour > 23 {
|
|
||||||
return t, nil, newDecodeError(b[0:2], "hour cannot be greater 23")
|
|
||||||
}
|
|
||||||
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 t.Minute > 59 {
|
|
||||||
return t, nil, newDecodeError(b[3:5], "minutes cannot be greater 59")
|
|
||||||
}
|
|
||||||
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 t.Second > 59 {
|
|
||||||
return t, nil, newDecodeError(b[3:5], "seconds cannot be greater 59")
|
|
||||||
}
|
|
||||||
|
|
||||||
const minLengthWithFrac = 9
|
|
||||||
if len(b) >= minLengthWithFrac && b[minLengthWithFrac-1] == '.' {
|
|
||||||
frac := 0
|
|
||||||
digits := 0
|
|
||||||
|
|
||||||
for i, c := range b[minLengthWithFrac:] {
|
|
||||||
if !isDigit(c) {
|
|
||||||
if i == 0 {
|
|
||||||
return t, nil, newDecodeError(b[i:i+1], "need at least one digit after fraction point")
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxFracPrecision = 9
|
|
||||||
if i >= maxFracPrecision {
|
|
||||||
return t, nil, newDecodeError(b[i:i+1], "maximum precision for date time is nanosecond")
|
|
||||||
}
|
|
||||||
|
|
||||||
frac *= 10
|
|
||||||
frac += int(c - '0')
|
|
||||||
digits++
|
|
||||||
}
|
|
||||||
|
|
||||||
if digits == 0 {
|
|
||||||
return t, nil, newDecodeError(b[minLengthWithFrac-1:minLengthWithFrac], "nanoseconds need at least one digit")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Nanosecond = frac * nspow[digits]
|
|
||||||
t.Precision = digits
|
|
||||||
|
|
||||||
return t, b[9+digits:], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return t, b[8:], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint:cyclop
|
|
||||||
func parseFloat(b []byte) (float64, error) {
|
|
||||||
if len(b) == 4 && (b[0] == '+' || b[0] == '-') && b[1] == 'n' && b[2] == 'a' && b[3] == 'n' {
|
|
||||||
return math.NaN(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cleaned, err := checkAndRemoveUnderscoresFloats(b)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if cleaned[0] == '.' {
|
|
||||||
return 0, newDecodeError(b, "float cannot start with a dot")
|
|
||||||
}
|
|
||||||
|
|
||||||
if cleaned[len(cleaned)-1] == '.' {
|
|
||||||
return 0, newDecodeError(b, "float cannot end with a dot")
|
|
||||||
}
|
|
||||||
|
|
||||||
dotAlreadySeen := false
|
|
||||||
for i, c := range cleaned {
|
|
||||||
if c == '.' {
|
|
||||||
if dotAlreadySeen {
|
|
||||||
return 0, newDecodeError(b[i:i+1], "float can have at most one decimal point")
|
|
||||||
}
|
|
||||||
if !isDigit(cleaned[i-1]) {
|
|
||||||
return 0, newDecodeError(b[i-1:i+1], "float decimal point must be preceded by a digit")
|
|
||||||
}
|
|
||||||
if !isDigit(cleaned[i+1]) {
|
|
||||||
return 0, newDecodeError(b[i:i+2], "float decimal point must be followed by a digit")
|
|
||||||
}
|
|
||||||
dotAlreadySeen = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
start := 0
|
|
||||||
if b[0] == '+' || b[0] == '-' {
|
|
||||||
start = 1
|
|
||||||
}
|
|
||||||
if b[start] == '0' && isDigit(b[start+1]) {
|
|
||||||
return 0, newDecodeError(b, "float integer part cannot have leading zeroes")
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := strconv.ParseFloat(string(cleaned), 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, newDecodeError(b, "unable to parse float: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return f, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseIntHex(b []byte) (int64, error) {
|
|
||||||
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
i, err := strconv.ParseInt(string(cleaned), 16, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, newDecodeError(b, "couldn't parse hexadecimal number: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseIntOct(b []byte) (int64, error) {
|
|
||||||
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
i, err := strconv.ParseInt(string(cleaned), 8, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, newDecodeError(b, "couldn't parse octal number: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseIntBin(b []byte) (int64, error) {
|
|
||||||
cleaned, err := checkAndRemoveUnderscoresIntegers(b[2:])
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
i, err := strconv.ParseInt(string(cleaned), 2, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, newDecodeError(b, "couldn't parse binary number: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isSign(b byte) bool {
|
|
||||||
return b == '+' || b == '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseIntDec(b []byte) (int64, error) {
|
|
||||||
cleaned, err := checkAndRemoveUnderscoresIntegers(b)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
startIdx := 0
|
|
||||||
|
|
||||||
if isSign(cleaned[0]) {
|
|
||||||
startIdx++
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cleaned) > startIdx+1 && cleaned[startIdx] == '0' {
|
|
||||||
return 0, newDecodeError(b, "leading zero not allowed on decimal number")
|
|
||||||
}
|
|
||||||
|
|
||||||
i, err := strconv.ParseInt(string(cleaned), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, newDecodeError(b, "couldn't parse decimal number: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkAndRemoveUnderscoresIntegers(b []byte) ([]byte, error) {
|
|
||||||
if b[0] == '_' {
|
|
||||||
return nil, newDecodeError(b[0:1], "number cannot start with underscore")
|
|
||||||
}
|
|
||||||
|
|
||||||
if b[len(b)-1] == '_' {
|
|
||||||
return nil, newDecodeError(b[len(b)-1:], "number cannot end with underscore")
|
|
||||||
}
|
|
||||||
|
|
||||||
// fast path
|
|
||||||
i := 0
|
|
||||||
for ; i < len(b); i++ {
|
|
||||||
if b[i] == '_' {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if i == len(b) {
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
before := false
|
|
||||||
cleaned := make([]byte, i, len(b))
|
|
||||||
copy(cleaned, b)
|
|
||||||
|
|
||||||
for i++; i < len(b); i++ {
|
|
||||||
c := b[i]
|
|
||||||
if c == '_' {
|
|
||||||
if !before {
|
|
||||||
return nil, newDecodeError(b[i-1:i+1], "number must have at least one digit between underscores")
|
|
||||||
}
|
|
||||||
before = false
|
|
||||||
} else {
|
|
||||||
before = true
|
|
||||||
cleaned = append(cleaned, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleaned, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkAndRemoveUnderscoresFloats(b []byte) ([]byte, error) {
|
|
||||||
if b[0] == '_' {
|
|
||||||
return nil, newDecodeError(b[0:1], "number cannot start with underscore")
|
|
||||||
}
|
|
||||||
|
|
||||||
if b[len(b)-1] == '_' {
|
|
||||||
return nil, newDecodeError(b[len(b)-1:], "number cannot end with underscore")
|
|
||||||
}
|
|
||||||
|
|
||||||
// fast path
|
|
||||||
i := 0
|
|
||||||
for ; i < len(b); i++ {
|
|
||||||
if b[i] == '_' {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if i == len(b) {
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
before := false
|
|
||||||
cleaned := make([]byte, 0, len(b))
|
|
||||||
|
|
||||||
for i := 0; i < len(b); i++ {
|
|
||||||
c := b[i]
|
|
||||||
|
|
||||||
switch c {
|
|
||||||
case '_':
|
|
||||||
if !before {
|
|
||||||
return nil, newDecodeError(b[i-1:i+1], "number must have at least one digit between underscores")
|
|
||||||
}
|
|
||||||
if i < len(b)-1 && (b[i+1] == 'e' || b[i+1] == 'E') {
|
|
||||||
return nil, newDecodeError(b[i+1:i+2], "cannot have underscore before exponent")
|
|
||||||
}
|
|
||||||
before = false
|
|
||||||
case 'e', 'E':
|
|
||||||
if i < len(b)-1 && b[i+1] == '_' {
|
|
||||||
return nil, newDecodeError(b[i+1:i+2], "cannot have underscore after exponent")
|
|
||||||
}
|
|
||||||
cleaned = append(cleaned, c)
|
|
||||||
case '.':
|
|
||||||
if i < len(b)-1 && b[i+1] == '_' {
|
|
||||||
return nil, newDecodeError(b[i+1:i+2], "cannot have underscore after decimal point")
|
|
||||||
}
|
|
||||||
if i > 0 && b[i-1] == '_' {
|
|
||||||
return nil, newDecodeError(b[i-1:i], "cannot have underscore before decimal point")
|
|
||||||
}
|
|
||||||
cleaned = append(cleaned, c)
|
|
||||||
default:
|
|
||||||
before = true
|
|
||||||
cleaned = append(cleaned, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleaned, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isValidDate checks if a provided date is a date that exists.
|
|
||||||
func isValidDate(year int, month int, day int) bool {
|
|
||||||
return day <= daysIn(month, year)
|
|
||||||
}
|
|
||||||
|
|
||||||
// daysBefore[m] counts the number of days in a non-leap year
|
|
||||||
// before month m begins. There is an entry for m=12, counting
|
|
||||||
// the number of days before January of next year (365).
|
|
||||||
var daysBefore = [...]int32{
|
|
||||||
0,
|
|
||||||
31,
|
|
||||||
31 + 28,
|
|
||||||
31 + 28 + 31,
|
|
||||||
31 + 28 + 31 + 30,
|
|
||||||
31 + 28 + 31 + 30 + 31,
|
|
||||||
31 + 28 + 31 + 30 + 31 + 30,
|
|
||||||
31 + 28 + 31 + 30 + 31 + 30 + 31,
|
|
||||||
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31,
|
|
||||||
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30,
|
|
||||||
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31,
|
|
||||||
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30,
|
|
||||||
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31,
|
|
||||||
}
|
|
||||||
|
|
||||||
func daysIn(m int, year int) int {
|
|
||||||
if m == 2 && isLeap(year) {
|
|
||||||
return 29
|
|
||||||
}
|
|
||||||
return int(daysBefore[m] - daysBefore[m-1])
|
|
||||||
}
|
|
||||||
|
|
||||||
func isLeap(year int) bool {
|
|
||||||
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
|
|
||||||
}
|
|
||||||
@@ -1,2 +1,23 @@
|
|||||||
// Package toml is a library to read and write TOML documents.
|
// 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
|
package toml
|
||||||
|
|||||||
+170
@@ -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"
|
||||||
|
}
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
package toml
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/pelletier/go-toml/v2/internal/danger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
key Key
|
|
||||||
|
|
||||||
human string
|
|
||||||
}
|
|
||||||
|
|
||||||
// StrictMissingError occurs in a TOML document that does not have a
|
|
||||||
// corresponding field in the target value. It contains all the missing fields
|
|
||||||
// in Errors.
|
|
||||||
//
|
|
||||||
// Emitted by Decoder when SetStrict(true) was called.
|
|
||||||
type StrictMissingError struct {
|
|
||||||
// One error per field that could not be found.
|
|
||||||
Errors []DecodeError
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error returns the canonical string for this error.
|
|
||||||
func (s *StrictMissingError) Error() string {
|
|
||||||
return "strict mode: fields in the document are missing in the target struct"
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a human readable description of all errors.
|
|
||||||
func (s *StrictMissingError) String() string {
|
|
||||||
var buf strings.Builder
|
|
||||||
|
|
||||||
for i, e := range s.Errors {
|
|
||||||
if i > 0 {
|
|
||||||
buf.WriteString("\n---\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteString(e.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
type Key []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
|
|
||||||
key Key // optional
|
|
||||||
}
|
|
||||||
|
|
||||||
func (de *decodeError) Error() string {
|
|
||||||
return de.message
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDecodeError(highlight []byte, format string, args ...interface{}) error {
|
|
||||||
return &decodeError{
|
|
||||||
highlight: highlight,
|
|
||||||
message: fmt.Errorf(format, args...).Error(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error returns the error message contained in the DecodeError.
|
|
||||||
func (e *DecodeError) Error() string {
|
|
||||||
return "toml: " + 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key that was being processed when the error occurred. The key is present only
|
|
||||||
// if this DecodeError is part of a StrictMissingError.
|
|
||||||
func (e *DecodeError) Key() Key {
|
|
||||||
return e.key
|
|
||||||
}
|
|
||||||
|
|
||||||
// decodeErrorFromHighlight creates a DecodeError referencing 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) *DecodeError {
|
|
||||||
offset := danger.SubsliceOffset(document, de.highlight)
|
|
||||||
|
|
||||||
errMessage := de.Error()
|
|
||||||
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))
|
|
||||||
|
|
||||||
// Write the lines of context strictly before the error.
|
|
||||||
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')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the document line that contains the error.
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
// Write the line with the error message itself (so it does not have a line
|
|
||||||
// number).
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the lines of context strictly after the error.
|
|
||||||
|
|
||||||
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,
|
|
||||||
key: de.key,
|
|
||||||
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:
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
-226
@@ -1,226 +0,0 @@
|
|||||||
package toml
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
//nolint:funlen
|
|
||||||
func TestDecodeError(t *testing.T) {
|
|
||||||
|
|
||||||
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`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "handle remainder of the error line when there is only one line",
|
|
||||||
doc: [3]string{`P=`, `[`, `#`},
|
|
||||||
msg: "array is incomplete",
|
|
||||||
expected: `1| P=[#
|
|
||||||
| ~ array is incomplete`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, e := range examples {
|
|
||||||
e := e
|
|
||||||
t.Run(e.desc, func(t *testing.T) {
|
|
||||||
|
|
||||||
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())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDecodeError_Accessors(t *testing.T) {
|
|
||||||
|
|
||||||
e := DecodeError{
|
|
||||||
message: "foo",
|
|
||||||
line: 1,
|
|
||||||
column: 2,
|
|
||||||
key: []string{"one", "two"},
|
|
||||||
human: "bar",
|
|
||||||
}
|
|
||||||
assert.Equal(t, "toml: foo", e.Error())
|
|
||||||
r, c := e.Position()
|
|
||||||
assert.Equal(t, 1, r)
|
|
||||||
assert.Equal(t, 2, c)
|
|
||||||
assert.Equal(t, Key{"one", "two"}, e.Key())
|
|
||||||
assert.Equal(t, "bar", e.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleDecodeError() {
|
|
||||||
doc := `name = 123__456`
|
|
||||||
|
|
||||||
s := map[string]interface{}{}
|
|
||||||
err := Unmarshal([]byte(doc), &s)
|
|
||||||
|
|
||||||
fmt.Println(err)
|
|
||||||
|
|
||||||
//nolint:errorlint
|
|
||||||
de := err.(*DecodeError)
|
|
||||||
fmt.Println(de.String())
|
|
||||||
|
|
||||||
row, col := de.Position()
|
|
||||||
fmt.Println("error occurred at row", row, "column", col)
|
|
||||||
// Output:
|
|
||||||
// toml: number must have at least one digit between underscores
|
|
||||||
// 1| name = 123__456
|
|
||||||
// | ~~ number must have at least one digit between underscores
|
|
||||||
// error occurred at row 1 column 11
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
-100
@@ -1,100 +0,0 @@
|
|||||||
package toml_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/pelletier/go-toml/v2"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFastSimple(t *testing.T) {
|
|
||||||
m := map[string]int64{}
|
|
||||||
err := toml.Unmarshal([]byte(`a = 42`), &m)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, map[string]int64{"a": 42}, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFastSimpleString(t *testing.T) {
|
|
||||||
m := map[string]string{}
|
|
||||||
err := toml.Unmarshal([]byte(`a = "hello"`), &m)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, map[string]string{"a": "hello"}, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFastSimpleInterface(t *testing.T) {
|
|
||||||
m := map[string]interface{}{}
|
|
||||||
err := toml.Unmarshal([]byte(`
|
|
||||||
a = "hello"
|
|
||||||
b = 42`), &m)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, map[string]interface{}{
|
|
||||||
"a": "hello",
|
|
||||||
"b": int64(42),
|
|
||||||
}, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFastMultipartKeyInterface(t *testing.T) {
|
|
||||||
m := map[string]interface{}{}
|
|
||||||
err := toml.Unmarshal([]byte(`
|
|
||||||
a.interim = "test"
|
|
||||||
a.b.c = "hello"
|
|
||||||
b = 42`), &m)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, map[string]interface{}{
|
|
||||||
"a": map[string]interface{}{
|
|
||||||
"interim": "test",
|
|
||||||
"b": map[string]interface{}{
|
|
||||||
"c": "hello",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"b": int64(42),
|
|
||||||
}, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFastExistingMap(t *testing.T) {
|
|
||||||
m := map[string]interface{}{
|
|
||||||
"ints": map[string]int{},
|
|
||||||
}
|
|
||||||
err := toml.Unmarshal([]byte(`
|
|
||||||
ints.one = 1
|
|
||||||
ints.two = 2
|
|
||||||
strings.yo = "hello"`), &m)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, map[string]interface{}{
|
|
||||||
"ints": map[string]interface{}{
|
|
||||||
"one": int64(1),
|
|
||||||
"two": int64(2),
|
|
||||||
},
|
|
||||||
"strings": map[string]interface{}{
|
|
||||||
"yo": "hello",
|
|
||||||
},
|
|
||||||
}, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFastArrayTable(t *testing.T) {
|
|
||||||
b := []byte(`
|
|
||||||
[root]
|
|
||||||
[[root.nested]]
|
|
||||||
name = 'Bob'
|
|
||||||
[[root.nested]]
|
|
||||||
name = 'Alice'
|
|
||||||
`)
|
|
||||||
|
|
||||||
m := map[string]interface{}{}
|
|
||||||
|
|
||||||
err := toml.Unmarshal(b, &m)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, map[string]interface{}{
|
|
||||||
"root": map[string]interface{}{
|
|
||||||
"nested": []interface{}{
|
|
||||||
map[string]interface{}{
|
|
||||||
"name": "Bob",
|
|
||||||
},
|
|
||||||
map[string]interface{}{
|
|
||||||
"name": "Alice",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, m)
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
module github.com/pelletier/go-toml/v2
|
module github.com/pelletier/go-toml
|
||||||
|
|
||||||
go 1.16
|
go 1.12
|
||||||
|
|
||||||
// latest (v1.7.0) doesn't have the fix for time.Time
|
|
||||||
require github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942
|
|
||||||
|
|||||||
@@ -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.1-0.20210427113832-6241f9ab9942 h1:t0lM6y/M5IiUZyvbBTcngso8SZEZICH7is9B6g/obVU=
|
|
||||||
github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942/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=
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
package ast
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/pelletier/go-toml/v2/internal/danger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsLast returns true if the current node of the iterator is the last one.
|
|
||||||
// Subsequent call to Next() will return false.
|
|
||||||
func (c *Iterator) IsLast() bool {
|
|
||||||
return c.node.next == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 Reference) *Node {
|
|
||||||
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
|
|
||||||
Raw Range // Raw bytes from the input.
|
|
||||||
Data []byte // Node value (could be either allocated or referencing the input).
|
|
||||||
|
|
||||||
// References to other nodes, as offsets in the backing array from this
|
|
||||||
// node. References can go backward, so those can be negative.
|
|
||||||
next int // 0 if last element
|
|
||||||
child int // 0 if no child
|
|
||||||
}
|
|
||||||
|
|
||||||
type Range struct {
|
|
||||||
Offset uint32
|
|
||||||
Length uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 nil
|
|
||||||
}
|
|
||||||
ptr := unsafe.Pointer(n)
|
|
||||||
size := unsafe.Sizeof(Node{})
|
|
||||||
return (*Node)(danger.Stride(ptr, size, 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 nil
|
|
||||||
}
|
|
||||||
ptr := unsafe.Pointer(n)
|
|
||||||
size := unsafe.Sizeof(Node{})
|
|
||||||
return (*Node)(danger.Stride(ptr, size, n.child))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valid returns true if the node's kind is set (not to Invalid).
|
|
||||||
func (n *Node) Valid() bool {
|
|
||||||
return n != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
return n.Child()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Children returns an iterator over a node's children.
|
|
||||||
func (n *Node) Children() Iterator {
|
|
||||||
return Iterator{node: n.Child()}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package ast
|
|
||||||
|
|
||||||
type Reference int
|
|
||||||
|
|
||||||
const InvalidReference Reference = -1
|
|
||||||
|
|
||||||
func (r Reference) Valid() bool {
|
|
||||||
return r != InvalidReference
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Builder) Reset() {
|
|
||||||
b.tree.nodes = b.tree.nodes[:0]
|
|
||||||
b.lastIdx = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Builder) Push(n Node) Reference {
|
|
||||||
b.lastIdx = len(b.tree.nodes)
|
|
||||||
b.tree.nodes = append(b.tree.nodes, n)
|
|
||||||
return Reference(b.lastIdx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Builder) PushAndChain(n Node) Reference {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
b.lastIdx = newIdx
|
|
||||||
return Reference(b.lastIdx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Builder) AttachChild(parent Reference, child Reference) {
|
|
||||||
b.tree.nodes[parent].child = int(child) - int(parent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Builder) Chain(from Reference, to Reference) {
|
|
||||||
b.tree.nodes[from].next = int(to) - int(from)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
LocalTime
|
|
||||||
LocalDateTime
|
|
||||||
DateTime
|
|
||||||
)
|
|
||||||
|
|
||||||
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 LocalTime:
|
|
||||||
return "LocalTime"
|
|
||||||
case LocalDateTime:
|
|
||||||
return "LocalDateTime"
|
|
||||||
case DateTime:
|
|
||||||
return "DateTime"
|
|
||||||
}
|
|
||||||
panic(fmt.Errorf("Kind.String() not implemented for '%d'", k))
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
package danger
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func BytesRange(start []byte, end []byte) []byte {
|
|
||||||
if start == nil || end == nil {
|
|
||||||
panic("cannot call BytesRange with nil")
|
|
||||||
}
|
|
||||||
startp := (*reflect.SliceHeader)(unsafe.Pointer(&start))
|
|
||||||
endp := (*reflect.SliceHeader)(unsafe.Pointer(&end))
|
|
||||||
|
|
||||||
if startp.Data > endp.Data {
|
|
||||||
panic(fmt.Errorf("start pointer address (%d) is after end pointer address (%d)", startp.Data, endp.Data))
|
|
||||||
}
|
|
||||||
|
|
||||||
l := startp.Len
|
|
||||||
endLen := int(endp.Data-startp.Data) + endp.Len
|
|
||||||
if endLen > l {
|
|
||||||
l = endLen
|
|
||||||
}
|
|
||||||
|
|
||||||
if l > startp.Cap {
|
|
||||||
panic(fmt.Errorf("range length is larger than capacity"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return start[:l]
|
|
||||||
}
|
|
||||||
|
|
||||||
func Stride(ptr unsafe.Pointer, size uintptr, offset int) unsafe.Pointer {
|
|
||||||
// TODO: replace with unsafe.Add when Go 1.17 is released
|
|
||||||
// https://github.com/golang/go/issues/40481
|
|
||||||
return unsafe.Pointer(uintptr(ptr) + uintptr(int(size)*offset))
|
|
||||||
}
|
|
||||||
|
|
||||||
type Slice struct {
|
|
||||||
Data unsafe.Pointer
|
|
||||||
Len int
|
|
||||||
Cap int
|
|
||||||
}
|
|
||||||
|
|
||||||
type iface struct {
|
|
||||||
typ unsafe.Pointer
|
|
||||||
ptr unsafe.Pointer
|
|
||||||
}
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
package danger_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/pelletier/go-toml/v2/internal/danger"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSubsliceOffsetValid(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 := danger.SubsliceOffset(d, s)
|
|
||||||
assert.Equal(t, e.offset, offset)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubsliceOffsetInvalid(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() {
|
|
||||||
danger.SubsliceOffset(d, s)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStride(t *testing.T) {
|
|
||||||
a := []byte{1, 2, 3, 4}
|
|
||||||
x := &a[1]
|
|
||||||
n := (*byte)(danger.Stride(unsafe.Pointer(x), unsafe.Sizeof(byte(0)), 1))
|
|
||||||
require.Equal(t, &a[2], n)
|
|
||||||
n = (*byte)(danger.Stride(unsafe.Pointer(x), unsafe.Sizeof(byte(0)), -1))
|
|
||||||
require.Equal(t, &a[0], n)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBytesRange(t *testing.T) {
|
|
||||||
type fn = func() ([]byte, []byte)
|
|
||||||
examples := []struct {
|
|
||||||
desc string
|
|
||||||
test fn
|
|
||||||
expected []byte
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "simple",
|
|
||||||
test: func() ([]byte, []byte) {
|
|
||||||
full := []byte("hello world")
|
|
||||||
return full[1:3], full[6:8]
|
|
||||||
},
|
|
||||||
expected: []byte("ello wo"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "full",
|
|
||||||
test: func() ([]byte, []byte) {
|
|
||||||
full := []byte("hello world")
|
|
||||||
return full[0:1], full[len(full)-1:]
|
|
||||||
},
|
|
||||||
expected: []byte("hello world"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "end before start",
|
|
||||||
test: func() ([]byte, []byte) {
|
|
||||||
full := []byte("hello world")
|
|
||||||
return full[len(full)-1:], full[0:1]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "nils",
|
|
||||||
test: func() ([]byte, []byte) {
|
|
||||||
return nil, nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "nils start",
|
|
||||||
test: func() ([]byte, []byte) {
|
|
||||||
return nil, []byte("foo")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "nils end",
|
|
||||||
test: func() ([]byte, []byte) {
|
|
||||||
return []byte("foo"), nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "start is end",
|
|
||||||
test: func() ([]byte, []byte) {
|
|
||||||
full := []byte("hello world")
|
|
||||||
return full[1:3], full[1:3]
|
|
||||||
},
|
|
||||||
expected: []byte("el"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "end contained in start",
|
|
||||||
test: func() ([]byte, []byte) {
|
|
||||||
full := []byte("hello world")
|
|
||||||
return full[1:7], full[2:4]
|
|
||||||
},
|
|
||||||
expected: []byte("ello w"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "different backing arrays",
|
|
||||||
test: func() ([]byte, []byte) {
|
|
||||||
one := []byte("hello world")
|
|
||||||
two := []byte("hello world")
|
|
||||||
return one, two
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, e := range examples {
|
|
||||||
t.Run(e.desc, func(t *testing.T) {
|
|
||||||
start, end := e.test()
|
|
||||||
if e.expected == nil {
|
|
||||||
require.Panics(t, func() {
|
|
||||||
danger.BytesRange(start, end)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
res := danger.BytesRange(start, end)
|
|
||||||
require.Equal(t, e.expected, res)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
//go:build go1.18
|
|
||||||
// +build go1.18
|
|
||||||
|
|
||||||
package danger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExtendSlice(t reflect.Type, s *Slice, n int) Slice {
|
|
||||||
arrayType := reflect.ArrayOf(n, t.Elem())
|
|
||||||
arrayData := reflect.New(arrayType)
|
|
||||||
reflect.Copy(arrayData.Elem(), reflect.NewAt(t, unsafe.Pointer(s)).Elem())
|
|
||||||
return Slice{
|
|
||||||
Data: unsafe.Pointer(arrayData.Pointer()),
|
|
||||||
Len: s.Len,
|
|
||||||
Cap: n,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
//go:build !go1.18
|
|
||||||
// +build !go1.18
|
|
||||||
|
|
||||||
package danger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:linkname unsafe_NewArray reflect.unsafe_NewArray
|
|
||||||
func unsafe_NewArray(rtype unsafe.Pointer, length int) unsafe.Pointer
|
|
||||||
|
|
||||||
//go:linkname typedslicecopy reflect.typedslicecopy
|
|
||||||
//go:noescape
|
|
||||||
func typedslicecopy(elemType unsafe.Pointer, dst, src Slice) int
|
|
||||||
|
|
||||||
func ExtendSlice(t reflect.Type, s *Slice, n int) Slice {
|
|
||||||
elemTypeRef := t.Elem()
|
|
||||||
elemTypePtr := ((*iface)(unsafe.Pointer(&elemTypeRef))).ptr
|
|
||||||
|
|
||||||
d := Slice{
|
|
||||||
Data: unsafe_NewArray(elemTypePtr, n),
|
|
||||||
Len: s.Len,
|
|
||||||
Cap: n,
|
|
||||||
}
|
|
||||||
|
|
||||||
typedslicecopy(elemTypePtr, d, *s)
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package danger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
// typeID is used as key in encoder and decoder caches to enable using
|
|
||||||
// the optimize runtime.mapaccess2_fast64 function instead of the more
|
|
||||||
// expensive lookup if we were to use reflect.Type as map key.
|
|
||||||
//
|
|
||||||
// typeID holds the pointer to the reflect.Type value, which is unique
|
|
||||||
// in the program.
|
|
||||||
//
|
|
||||||
// https://github.com/segmentio/encoding/blob/master/json/codec.go#L59-L61
|
|
||||||
type TypeID unsafe.Pointer
|
|
||||||
|
|
||||||
func MakeTypeID(t reflect.Type) TypeID {
|
|
||||||
// reflect.Type has the fields:
|
|
||||||
// typ unsafe.Pointer
|
|
||||||
// ptr unsafe.Pointer
|
|
||||||
return TypeID((*[2]unsafe.Pointer)(unsafe.Pointer(&t))[1])
|
|
||||||
}
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
package imported_tests
|
|
||||||
|
|
||||||
// Those tests have been imported from v1, but adjust to match the new
|
|
||||||
// defaults of v2.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"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))
|
|
||||||
}
|
|
||||||
|
|
||||||
type textMarshaler struct {
|
|
||||||
FirstName string
|
|
||||||
LastName string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m textMarshaler) MarshalText() ([]byte, error) {
|
|
||||||
fullName := fmt.Sprintf("%s %s", m.FirstName, m.LastName)
|
|
||||||
return []byte(fullName), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTextMarshaler(t *testing.T) {
|
|
||||||
type wrap struct {
|
|
||||||
TM textMarshaler
|
|
||||||
}
|
|
||||||
|
|
||||||
m := textMarshaler{FirstName: "Sally", LastName: "Fields"}
|
|
||||||
|
|
||||||
t.Run("at root", func(t *testing.T) {
|
|
||||||
_, err := toml.Marshal(m)
|
|
||||||
// in v2 we do not allow TextMarshaler at root
|
|
||||||
require.Error(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("leaf", func(t *testing.T) {
|
|
||||||
res, err := toml.Marshal(wrap{m})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.Equal(t, "TM = 'Sally Fields'\n", string(res))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,50 +0,0 @@
|
|||||||
package tracker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/pelletier/go-toml/v2/internal/ast"
|
|
||||||
)
|
|
||||||
|
|
||||||
// KeyTracker is a tracker that keeps track of the current Key as the AST is
|
|
||||||
// walked.
|
|
||||||
type KeyTracker struct {
|
|
||||||
k []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateTable sets the state of the tracker with the AST table node.
|
|
||||||
func (t *KeyTracker) UpdateTable(node *ast.Node) {
|
|
||||||
t.reset()
|
|
||||||
t.Push(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateArrayTable sets the state of the tracker with the AST array table node.
|
|
||||||
func (t *KeyTracker) UpdateArrayTable(node *ast.Node) {
|
|
||||||
t.reset()
|
|
||||||
t.Push(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push the given key on the stack.
|
|
||||||
func (t *KeyTracker) Push(node *ast.Node) {
|
|
||||||
it := node.Key()
|
|
||||||
for it.Next() {
|
|
||||||
t.k = append(t.k, string(it.Node().Data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pop key from stack.
|
|
||||||
func (t *KeyTracker) Pop(node *ast.Node) {
|
|
||||||
it := node.Key()
|
|
||||||
for it.Next() {
|
|
||||||
t.k = t.k[:len(t.k)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key returns the current key
|
|
||||||
func (t *KeyTracker) Key() []string {
|
|
||||||
k := make([]string, len(t.k))
|
|
||||||
copy(k, t.k)
|
|
||||||
return k
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *KeyTracker) reset() {
|
|
||||||
t.k = t.k[:0]
|
|
||||||
}
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
package tracker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"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")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SeenTracker tracks which keys have been seen with which TOML type to flag
|
|
||||||
// duplicates and mismatches according to the spec.
|
|
||||||
//
|
|
||||||
// Each node in the visited tree is represented by an entry. Each entry has an
|
|
||||||
// identifier, which is provided by a counter. Entries are stored in the array
|
|
||||||
// entries. As new nodes are discovered (referenced for the first time in the
|
|
||||||
// TOML document), entries are created and appended to the array. An entry
|
|
||||||
// points to its parent using its id.
|
|
||||||
//
|
|
||||||
// To find whether a given key (sequence of []byte) has already been visited,
|
|
||||||
// the entries are linearly searched, looking for one with the right name and
|
|
||||||
// parent id.
|
|
||||||
//
|
|
||||||
// Given that all keys appear in the document after their parent, it is
|
|
||||||
// guaranteed that all descendants of a node are stored after the node, this
|
|
||||||
// speeds up the search process.
|
|
||||||
//
|
|
||||||
// When encountering [[array tables]], the descendants of that node are removed
|
|
||||||
// to allow that branch of the tree to be "rediscovered". To maintain the
|
|
||||||
// invariant above, the deletion process needs to keep the order of entries.
|
|
||||||
// This results in more copies in that case.
|
|
||||||
type SeenTracker struct {
|
|
||||||
entries []entry
|
|
||||||
currentIdx int
|
|
||||||
nextID int
|
|
||||||
}
|
|
||||||
|
|
||||||
type entry struct {
|
|
||||||
id int
|
|
||||||
parent int
|
|
||||||
name []byte
|
|
||||||
kind keyKind
|
|
||||||
explicit bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove all descendants of node at position idx.
|
|
||||||
func (s *SeenTracker) clear(idx int) {
|
|
||||||
p := s.entries[idx].id
|
|
||||||
rest := clear(p, s.entries[idx+1:])
|
|
||||||
s.entries = s.entries[:idx+1+len(rest)]
|
|
||||||
}
|
|
||||||
|
|
||||||
func clear(parentID int, entries []entry) []entry {
|
|
||||||
for i := 0; i < len(entries); {
|
|
||||||
if entries[i].parent == parentID {
|
|
||||||
id := entries[i].id
|
|
||||||
copy(entries[i:], entries[i+1:])
|
|
||||||
entries = entries[:len(entries)-1]
|
|
||||||
rest := clear(id, entries[i:])
|
|
||||||
entries = entries[:i+len(rest)]
|
|
||||||
} else {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return entries
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SeenTracker) create(parentIdx int, name []byte, kind keyKind, explicit bool) int {
|
|
||||||
parentID := s.id(parentIdx)
|
|
||||||
|
|
||||||
idx := len(s.entries)
|
|
||||||
s.entries = append(s.entries, entry{
|
|
||||||
id: s.nextID,
|
|
||||||
parent: parentID,
|
|
||||||
name: name,
|
|
||||||
kind: kind,
|
|
||||||
explicit: explicit,
|
|
||||||
})
|
|
||||||
s.nextID++
|
|
||||||
return idx
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 *SeenTracker) CheckExpression(node *ast.Node) error {
|
|
||||||
if s.entries == nil {
|
|
||||||
// Skip ID = 0 to remove the confusion between nodes whose
|
|
||||||
// parent has id 0 and root nodes (parent id is 0 because it's
|
|
||||||
// the zero value).
|
|
||||||
s.nextID = 1
|
|
||||||
// Start unscoped, so idx is negative.
|
|
||||||
s.currentIdx = -1
|
|
||||||
}
|
|
||||||
switch node.Kind {
|
|
||||||
case ast.KeyValue:
|
|
||||||
return s.checkKeyValue(s.currentIdx, 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 *SeenTracker) checkTable(node *ast.Node) error {
|
|
||||||
it := node.Key()
|
|
||||||
|
|
||||||
parentIdx := -1
|
|
||||||
|
|
||||||
// This code is duplicated in checkArrayTable. This is because factoring
|
|
||||||
// it in a function requires to copy the iterator, or allocate it to the
|
|
||||||
// heap, which is not cheap.
|
|
||||||
for it.Next() {
|
|
||||||
if it.IsLast() {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
k := it.Node().Data
|
|
||||||
|
|
||||||
idx := s.find(parentIdx, k)
|
|
||||||
|
|
||||||
if idx < 0 {
|
|
||||||
idx = s.create(parentIdx, k, tableKind, false)
|
|
||||||
}
|
|
||||||
parentIdx = idx
|
|
||||||
}
|
|
||||||
|
|
||||||
k := it.Node().Data
|
|
||||||
idx := s.find(parentIdx, k)
|
|
||||||
|
|
||||||
if idx >= 0 {
|
|
||||||
kind := s.entries[idx].kind
|
|
||||||
if kind != tableKind {
|
|
||||||
return fmt.Errorf("toml: key %s should be a table, not a %s", string(k), kind)
|
|
||||||
}
|
|
||||||
if s.entries[idx].explicit {
|
|
||||||
return fmt.Errorf("toml: table %s already exists", string(k))
|
|
||||||
}
|
|
||||||
s.entries[idx].explicit = true
|
|
||||||
} else {
|
|
||||||
idx = s.create(parentIdx, k, tableKind, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.currentIdx = idx
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SeenTracker) checkArrayTable(node *ast.Node) error {
|
|
||||||
it := node.Key()
|
|
||||||
|
|
||||||
parentIdx := -1
|
|
||||||
|
|
||||||
for it.Next() {
|
|
||||||
if it.IsLast() {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
k := it.Node().Data
|
|
||||||
|
|
||||||
idx := s.find(parentIdx, k)
|
|
||||||
|
|
||||||
if idx < 0 {
|
|
||||||
idx = s.create(parentIdx, k, tableKind, false)
|
|
||||||
}
|
|
||||||
parentIdx = idx
|
|
||||||
}
|
|
||||||
|
|
||||||
k := it.Node().Data
|
|
||||||
idx := s.find(parentIdx, k)
|
|
||||||
|
|
||||||
if idx >= 0 {
|
|
||||||
kind := s.entries[idx].kind
|
|
||||||
if kind != arrayTableKind {
|
|
||||||
return fmt.Errorf("toml: key %s already exists as a %s, but should be an array table", kind, string(k))
|
|
||||||
}
|
|
||||||
s.clear(idx)
|
|
||||||
} else {
|
|
||||||
idx = s.create(parentIdx, k, arrayTableKind, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.currentIdx = idx
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SeenTracker) checkKeyValue(parentIdx int, node *ast.Node) error {
|
|
||||||
it := node.Key()
|
|
||||||
|
|
||||||
for it.Next() {
|
|
||||||
k := it.Node().Data
|
|
||||||
|
|
||||||
idx := s.find(parentIdx, k)
|
|
||||||
|
|
||||||
if idx < 0 {
|
|
||||||
idx = s.create(parentIdx, k, tableKind, false)
|
|
||||||
} else {
|
|
||||||
entry := s.entries[idx]
|
|
||||||
if it.IsLast() {
|
|
||||||
return fmt.Errorf("toml: key %s is already defined", string(k))
|
|
||||||
} else if entry.kind != tableKind {
|
|
||||||
return fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
|
|
||||||
} else if entry.explicit {
|
|
||||||
return fmt.Errorf("toml: cannot redefine table %s that has already been explicitly defined", string(k))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parentIdx = idx
|
|
||||||
}
|
|
||||||
|
|
||||||
s.entries[parentIdx].kind = valueKind
|
|
||||||
|
|
||||||
value := node.Value()
|
|
||||||
|
|
||||||
switch value.Kind {
|
|
||||||
case ast.InlineTable:
|
|
||||||
return s.checkInlineTable(parentIdx, value)
|
|
||||||
case ast.Array:
|
|
||||||
return s.checkArray(parentIdx, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SeenTracker) checkArray(parentIdx int, node *ast.Node) error {
|
|
||||||
set := false
|
|
||||||
it := node.Children()
|
|
||||||
for it.Next() {
|
|
||||||
if set {
|
|
||||||
s.clear(parentIdx)
|
|
||||||
}
|
|
||||||
n := it.Node()
|
|
||||||
switch n.Kind {
|
|
||||||
case ast.InlineTable:
|
|
||||||
err := s.checkInlineTable(parentIdx, n)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
set = true
|
|
||||||
case ast.Array:
|
|
||||||
err := s.checkArray(parentIdx, n)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
set = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SeenTracker) checkInlineTable(parentIdx int, node *ast.Node) error {
|
|
||||||
it := node.Children()
|
|
||||||
for it.Next() {
|
|
||||||
n := it.Node()
|
|
||||||
err := s.checkKeyValue(parentIdx, n)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SeenTracker) id(idx int) int {
|
|
||||||
if idx >= 0 {
|
|
||||||
return s.entries[idx].id
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SeenTracker) find(parentIdx int, k []byte) int {
|
|
||||||
parentID := s.id(parentIdx)
|
|
||||||
|
|
||||||
for i := parentIdx + 1; i < len(s.entries); i++ {
|
|
||||||
if s.entries[i].parent == parentID && bytes.Equal(s.entries[i].name, k) {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
package tracker
|
|
||||||
+112
@@ -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)
|
||||||
|
}
|
||||||
@@ -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{""})
|
||||||
|
}
|
||||||
+1247
File diff suppressed because it is too large
Load Diff
+242
-81
@@ -1,120 +1,281 @@
|
|||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// Copyright 2016 Google LLC
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// Package civil implements types for civil time, a time-zone-independent
|
||||||
|
// representation of time that follows the rules of the proleptic
|
||||||
|
// Gregorian calendar with exactly 24-hour days, 60-minute hours, and 60-second
|
||||||
|
// minutes.
|
||||||
|
//
|
||||||
|
// Because they lack location information, these types do not represent unique
|
||||||
|
// moments or intervals of time. Use time.Time for that purpose.
|
||||||
package toml
|
package toml
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LocalDate represents a calendar day in no specific timezone.
|
// A LocalDate represents a date (year, month, day).
|
||||||
|
//
|
||||||
|
// This type does not include location information, and therefore does not
|
||||||
|
// describe a unique 24-hour timespan.
|
||||||
type LocalDate struct {
|
type LocalDate struct {
|
||||||
Year int
|
Year int // Year (e.g., 2014).
|
||||||
Month int
|
Month time.Month // Month of the year (January = 1, ...).
|
||||||
Day int
|
Day int // Day of the month, starting at 1.
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsTime converts d into a specific time instance at midnight in zone.
|
// LocalDateOf returns the LocalDate in which a time occurs in that time's location.
|
||||||
func (d LocalDate) AsTime(zone *time.Location) time.Time {
|
func LocalDateOf(t time.Time) LocalDate {
|
||||||
return time.Date(d.Year, time.Month(d.Month), d.Day, 0, 0, 0, 0, zone)
|
var d LocalDate
|
||||||
|
d.Year, d.Month, d.Day = t.Date()
|
||||||
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns RFC 3339 representation of d.
|
// ParseLocalDate parses a string in RFC3339 full-date format and returns the date value it represents.
|
||||||
|
func ParseLocalDate(s string) (LocalDate, error) {
|
||||||
|
t, err := time.Parse("2006-01-02", s)
|
||||||
|
if err != nil {
|
||||||
|
return LocalDate{}, err
|
||||||
|
}
|
||||||
|
return LocalDateOf(t), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the date in RFC3339 full-date format.
|
||||||
func (d LocalDate) String() string {
|
func (d LocalDate) String() string {
|
||||||
return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day)
|
return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalText returns RFC 3339 representation of d.
|
// IsValid reports whether the date is valid.
|
||||||
|
func (d LocalDate) IsValid() bool {
|
||||||
|
return LocalDateOf(d.In(time.UTC)) == d
|
||||||
|
}
|
||||||
|
|
||||||
|
// In returns the time corresponding to time 00:00:00 of the date in the location.
|
||||||
|
//
|
||||||
|
// In is always consistent with time.LocalDate, even when time.LocalDate returns a time
|
||||||
|
// on a different day. For example, if loc is America/Indiana/Vincennes, then both
|
||||||
|
// time.LocalDate(1955, time.May, 1, 0, 0, 0, 0, loc)
|
||||||
|
// and
|
||||||
|
// civil.LocalDate{Year: 1955, Month: time.May, Day: 1}.In(loc)
|
||||||
|
// return 23:00:00 on April 30, 1955.
|
||||||
|
//
|
||||||
|
// In panics if loc is nil.
|
||||||
|
func (d LocalDate) In(loc *time.Location) time.Time {
|
||||||
|
return time.Date(d.Year, d.Month, d.Day, 0, 0, 0, 0, loc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddDays returns the date that is n days in the future.
|
||||||
|
// n can also be negative to go into the past.
|
||||||
|
func (d LocalDate) AddDays(n int) LocalDate {
|
||||||
|
return LocalDateOf(d.In(time.UTC).AddDate(0, 0, n))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DaysSince returns the signed number of days between the date and s, not including the end day.
|
||||||
|
// This is the inverse operation to AddDays.
|
||||||
|
func (d LocalDate) DaysSince(s LocalDate) (days int) {
|
||||||
|
// We convert to Unix time so we do not have to worry about leap seconds:
|
||||||
|
// Unix time increases by exactly 86400 seconds per day.
|
||||||
|
deltaUnix := d.In(time.UTC).Unix() - s.In(time.UTC).Unix()
|
||||||
|
return int(deltaUnix / 86400)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before reports whether d1 occurs before d2.
|
||||||
|
func (d1 LocalDate) Before(d2 LocalDate) bool {
|
||||||
|
if d1.Year != d2.Year {
|
||||||
|
return d1.Year < d2.Year
|
||||||
|
}
|
||||||
|
if d1.Month != d2.Month {
|
||||||
|
return d1.Month < d2.Month
|
||||||
|
}
|
||||||
|
return d1.Day < d2.Day
|
||||||
|
}
|
||||||
|
|
||||||
|
// After reports whether d1 occurs after d2.
|
||||||
|
func (d1 LocalDate) After(d2 LocalDate) bool {
|
||||||
|
return d2.Before(d1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements the encoding.TextMarshaler interface.
|
||||||
|
// The output is the result of d.String().
|
||||||
func (d LocalDate) MarshalText() ([]byte, error) {
|
func (d LocalDate) MarshalText() ([]byte, error) {
|
||||||
return []byte(d.String()), nil
|
return []byte(d.String()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalText parses b using RFC 3339 to fill d.
|
// UnmarshalText implements the encoding.TextUnmarshaler interface.
|
||||||
func (d *LocalDate) UnmarshalText(b []byte) error {
|
// The date is expected to be a string in a format accepted by ParseLocalDate.
|
||||||
res, err := parseLocalDate(b)
|
func (d *LocalDate) UnmarshalText(data []byte) error {
|
||||||
if err != nil {
|
var err error
|
||||||
return err
|
*d, err = ParseLocalDate(string(data))
|
||||||
}
|
return err
|
||||||
*d = res
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocalTime represents a time of day of no specific day in no specific
|
// A LocalTime represents a time with nanosecond precision.
|
||||||
// timezone.
|
//
|
||||||
|
// This type does not include location information, and therefore does not
|
||||||
|
// describe a unique moment in time.
|
||||||
|
//
|
||||||
|
// This type exists to represent the TIME type in storage-based APIs like BigQuery.
|
||||||
|
// Most operations on Times are unlikely to be meaningful. Prefer the LocalDateTime type.
|
||||||
type LocalTime struct {
|
type LocalTime struct {
|
||||||
Hour int // Hour of the day: [0; 24[
|
Hour int // The hour of the day in 24-hour format; range [0-23]
|
||||||
Minute int // Minute of the hour: [0; 60[
|
Minute int // The minute of the hour; range [0-59]
|
||||||
Second int // Second of the minute: [0; 60[
|
Second int // The second of the minute; range [0-59]
|
||||||
Nanosecond int // Nanoseconds within the second: [0, 1000000000[
|
Nanosecond int // The nanosecond of the second; range [0-999999999]
|
||||||
Precision int // Number of digits to display for Nanosecond.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns RFC 3339 representation of d.
|
// LocalTimeOf returns the LocalTime representing the time of day in which a time occurs
|
||||||
// If d.Nanosecond and d.Precision are zero, the time won't have a nanosecond
|
// in that time's location. It ignores the date.
|
||||||
// component. If d.Nanosecond > 0 but d.Precision = 0, then the minimum number
|
func LocalTimeOf(t time.Time) LocalTime {
|
||||||
// of digits for nanoseconds is provided.
|
var tm LocalTime
|
||||||
func (d LocalTime) String() string {
|
tm.Hour, tm.Minute, tm.Second = t.Clock()
|
||||||
s := fmt.Sprintf("%02d:%02d:%02d", d.Hour, d.Minute, d.Second)
|
tm.Nanosecond = t.Nanosecond()
|
||||||
|
return tm
|
||||||
if d.Precision > 0 {
|
|
||||||
s += fmt.Sprintf(".%09d", d.Nanosecond)[:d.Precision+1]
|
|
||||||
} else if d.Nanosecond > 0 {
|
|
||||||
// Nanoseconds are specified, but precision is not provided. Use the
|
|
||||||
// minimum.
|
|
||||||
s += strings.Trim(fmt.Sprintf(".%09d", d.Nanosecond), "0")
|
|
||||||
}
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalText returns RFC 3339 representation of d.
|
// ParseLocalTime parses a string and returns the time value it represents.
|
||||||
func (d LocalTime) MarshalText() ([]byte, error) {
|
// ParseLocalTime accepts an extended form of the RFC3339 partial-time format. After
|
||||||
return []byte(d.String()), nil
|
// the HH:MM:SS part of the string, an optional fractional part may appear,
|
||||||
}
|
// consisting of a decimal point followed by one to nine decimal digits.
|
||||||
|
// (RFC3339 admits only one digit after the decimal point).
|
||||||
// UnmarshalText parses b using RFC 3339 to fill d.
|
func ParseLocalTime(s string) (LocalTime, error) {
|
||||||
func (d *LocalTime) UnmarshalText(b []byte) error {
|
t, err := time.Parse("15:04:05.999999999", s)
|
||||||
res, left, err := parseLocalTime(b)
|
|
||||||
if err == nil && len(left) != 0 {
|
|
||||||
err = newDecodeError(left, "extra characters")
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return LocalTime{}, err
|
||||||
}
|
}
|
||||||
*d = res
|
return LocalTimeOf(t), nil
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocalDateTime represents a time of a specific day in no specific timezone.
|
// String returns the date in the format described in ParseLocalTime. If Nanoseconds
|
||||||
|
// is zero, no fractional part will be generated. Otherwise, the result will
|
||||||
|
// end with a fractional part consisting of a decimal point and nine digits.
|
||||||
|
func (t LocalTime) String() string {
|
||||||
|
s := fmt.Sprintf("%02d:%02d:%02d", t.Hour, t.Minute, t.Second)
|
||||||
|
if t.Nanosecond == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s + fmt.Sprintf(".%09d", t.Nanosecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid reports whether the time is valid.
|
||||||
|
func (t LocalTime) IsValid() bool {
|
||||||
|
// Construct a non-zero time.
|
||||||
|
tm := time.Date(2, 2, 2, t.Hour, t.Minute, t.Second, t.Nanosecond, time.UTC)
|
||||||
|
return LocalTimeOf(tm) == t
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements the encoding.TextMarshaler interface.
|
||||||
|
// The output is the result of t.String().
|
||||||
|
func (t LocalTime) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(t.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements the encoding.TextUnmarshaler interface.
|
||||||
|
// The time is expected to be a string in a format accepted by ParseLocalTime.
|
||||||
|
func (t *LocalTime) UnmarshalText(data []byte) error {
|
||||||
|
var err error
|
||||||
|
*t, err = ParseLocalTime(string(data))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// A LocalDateTime represents a date and time.
|
||||||
|
//
|
||||||
|
// This type does not include location information, and therefore does not
|
||||||
|
// describe a unique moment in time.
|
||||||
type LocalDateTime struct {
|
type LocalDateTime struct {
|
||||||
LocalDate
|
Date LocalDate
|
||||||
LocalTime
|
Time LocalTime
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsTime converts d into a specific time instance in zone.
|
// Note: We deliberately do not embed LocalDate into LocalDateTime, to avoid promoting AddDays and Sub.
|
||||||
func (d LocalDateTime) AsTime(zone *time.Location) time.Time {
|
|
||||||
return time.Date(d.Year, time.Month(d.Month), d.Day, d.Hour, d.Minute, d.Second, d.Nanosecond, zone)
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns RFC 3339 representation of d.
|
// LocalDateTimeOf returns the LocalDateTime in which a time occurs in that time's location.
|
||||||
func (d LocalDateTime) String() string {
|
func LocalDateTimeOf(t time.Time) LocalDateTime {
|
||||||
return d.LocalDate.String() + "T" + d.LocalTime.String()
|
return LocalDateTime{
|
||||||
}
|
Date: LocalDateOf(t),
|
||||||
|
Time: LocalTimeOf(t),
|
||||||
// MarshalText returns RFC 3339 representation of d.
|
|
||||||
func (d LocalDateTime) MarshalText() ([]byte, error) {
|
|
||||||
return []byte(d.String()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalText parses b using RFC 3339 to fill d.
|
|
||||||
func (d *LocalDateTime) UnmarshalText(data []byte) error {
|
|
||||||
res, left, err := parseLocalDateTime(data)
|
|
||||||
if err == nil && len(left) != 0 {
|
|
||||||
err = newDecodeError(left, "extra characters")
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseLocalDateTime parses a string and returns the LocalDateTime it represents.
|
||||||
|
// ParseLocalDateTime accepts a variant of the RFC3339 date-time format that omits
|
||||||
|
// the time offset but includes an optional fractional time, as described in
|
||||||
|
// ParseLocalTime. Informally, the accepted format is
|
||||||
|
// YYYY-MM-DDTHH:MM:SS[.FFFFFFFFF]
|
||||||
|
// where the 'T' may be a lower-case 't'.
|
||||||
|
func ParseLocalDateTime(s string) (LocalDateTime, error) {
|
||||||
|
t, err := time.Parse("2006-01-02T15:04:05.999999999", s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
t, err = time.Parse("2006-01-02t15:04:05.999999999", s)
|
||||||
|
if err != nil {
|
||||||
|
return LocalDateTime{}, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return LocalDateTimeOf(t), nil
|
||||||
*d = res
|
}
|
||||||
return nil
|
|
||||||
|
// String returns the date in the format described in ParseLocalDate.
|
||||||
|
func (dt LocalDateTime) String() string {
|
||||||
|
return dt.Date.String() + "T" + dt.Time.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid reports whether the datetime is valid.
|
||||||
|
func (dt LocalDateTime) IsValid() bool {
|
||||||
|
return dt.Date.IsValid() && dt.Time.IsValid()
|
||||||
|
}
|
||||||
|
|
||||||
|
// In returns the time corresponding to the LocalDateTime in the given location.
|
||||||
|
//
|
||||||
|
// If the time is missing or ambigous at the location, In returns the same
|
||||||
|
// result as time.LocalDate. For example, if loc is America/Indiana/Vincennes, then
|
||||||
|
// both
|
||||||
|
// time.LocalDate(1955, time.May, 1, 0, 30, 0, 0, loc)
|
||||||
|
// and
|
||||||
|
// civil.LocalDateTime{
|
||||||
|
// civil.LocalDate{Year: 1955, Month: time.May, Day: 1}},
|
||||||
|
// civil.LocalTime{Minute: 30}}.In(loc)
|
||||||
|
// return 23:30:00 on April 30, 1955.
|
||||||
|
//
|
||||||
|
// In panics if loc is nil.
|
||||||
|
func (dt LocalDateTime) In(loc *time.Location) time.Time {
|
||||||
|
return time.Date(dt.Date.Year, dt.Date.Month, dt.Date.Day, dt.Time.Hour, dt.Time.Minute, dt.Time.Second, dt.Time.Nanosecond, loc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before reports whether dt1 occurs before dt2.
|
||||||
|
func (dt1 LocalDateTime) Before(dt2 LocalDateTime) bool {
|
||||||
|
return dt1.In(time.UTC).Before(dt2.In(time.UTC))
|
||||||
|
}
|
||||||
|
|
||||||
|
// After reports whether dt1 occurs after dt2.
|
||||||
|
func (dt1 LocalDateTime) After(dt2 LocalDateTime) bool {
|
||||||
|
return dt2.Before(dt1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements the encoding.TextMarshaler interface.
|
||||||
|
// The output is the result of dt.String().
|
||||||
|
func (dt LocalDateTime) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(dt.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements the encoding.TextUnmarshaler interface.
|
||||||
|
// The datetime is expected to be a string in a format accepted by ParseLocalDateTime
|
||||||
|
func (dt *LocalDateTime) UnmarshalText(data []byte) error {
|
||||||
|
var err error
|
||||||
|
*dt, err = ParseLocalDateTime(string(data))
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+428
-100
@@ -1,118 +1,446 @@
|
|||||||
package toml_test
|
// Copyright 2016 Google LLC
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
package toml
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pelletier/go-toml/v2"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLocalDate_AsTime(t *testing.T) {
|
func cmpEqual(x, y interface{}) bool {
|
||||||
d := toml.LocalDate{2021, 6, 8}
|
return reflect.DeepEqual(x, y)
|
||||||
cast := d.AsTime(time.UTC)
|
|
||||||
require.Equal(t, time.Date(2021, time.June, 8, 0, 0, 0, 0, time.UTC), cast)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLocalDate_String(t *testing.T) {
|
func TestDates(t *testing.T) {
|
||||||
d := toml.LocalDate{2021, 6, 8}
|
for _, test := range []struct {
|
||||||
require.Equal(t, "2021-06-08", d.String())
|
date LocalDate
|
||||||
}
|
loc *time.Location
|
||||||
|
wantStr string
|
||||||
func TestLocalDate_MarshalText(t *testing.T) {
|
wantTime time.Time
|
||||||
d := toml.LocalDate{2021, 6, 8}
|
}{
|
||||||
b, err := d.MarshalText()
|
{
|
||||||
require.NoError(t, err)
|
date: LocalDate{2014, 7, 29},
|
||||||
require.Equal(t, []byte("2021-06-08"), b)
|
loc: time.Local,
|
||||||
}
|
wantStr: "2014-07-29",
|
||||||
|
wantTime: time.Date(2014, time.July, 29, 0, 0, 0, 0, time.Local),
|
||||||
func TestLocalDate_UnmarshalMarshalText(t *testing.T) {
|
},
|
||||||
d := toml.LocalDate{}
|
{
|
||||||
err := d.UnmarshalText([]byte("2021-06-08"))
|
date: LocalDateOf(time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local)),
|
||||||
require.NoError(t, err)
|
loc: time.UTC,
|
||||||
require.Equal(t, toml.LocalDate{2021, 6, 8}, d)
|
wantStr: "2014-08-20",
|
||||||
|
wantTime: time.Date(2014, 8, 20, 0, 0, 0, 0, time.UTC),
|
||||||
err = d.UnmarshalText([]byte("what"))
|
},
|
||||||
require.Error(t, err)
|
{
|
||||||
}
|
date: LocalDateOf(time.Date(999, time.January, 26, 0, 0, 0, 0, time.Local)),
|
||||||
|
loc: time.UTC,
|
||||||
func TestLocalTime_String(t *testing.T) {
|
wantStr: "0999-01-26",
|
||||||
d := toml.LocalTime{20, 12, 1, 2, 9}
|
wantTime: time.Date(999, 1, 26, 0, 0, 0, 0, time.UTC),
|
||||||
require.Equal(t, "20:12:01.000000002", d.String())
|
},
|
||||||
d = toml.LocalTime{20, 12, 1, 0, 0}
|
} {
|
||||||
require.Equal(t, "20:12:01", d.String())
|
if got := test.date.String(); got != test.wantStr {
|
||||||
d = toml.LocalTime{20, 12, 1, 0, 9}
|
t.Errorf("%#v.String() = %q, want %q", test.date, got, test.wantStr)
|
||||||
require.Equal(t, "20:12:01.000000000", d.String())
|
}
|
||||||
d = toml.LocalTime{20, 12, 1, 100, 0}
|
if got := test.date.In(test.loc); !got.Equal(test.wantTime) {
|
||||||
require.Equal(t, "20:12:01.0000001", d.String())
|
t.Errorf("%#v.In(%v) = %v, want %v", test.date, test.loc, got, test.wantTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLocalTime_MarshalText(t *testing.T) {
|
|
||||||
d := toml.LocalTime{20, 12, 1, 2, 9}
|
|
||||||
b, err := d.MarshalText()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, []byte("20:12:01.000000002"), b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLocalTime_UnmarshalMarshalText(t *testing.T) {
|
|
||||||
d := toml.LocalTime{}
|
|
||||||
err := d.UnmarshalText([]byte("20:12:01.000000002"))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, toml.LocalTime{20, 12, 1, 2, 9}, d)
|
|
||||||
|
|
||||||
err = d.UnmarshalText([]byte("what"))
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
err = d.UnmarshalText([]byte("20:12:01.000000002 bad"))
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLocalTime_RoundTrip(t *testing.T) {
|
|
||||||
var d struct{ A toml.LocalTime }
|
|
||||||
err := toml.Unmarshal([]byte("a=20:12:01.500"), &d)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, "20:12:01.500", d.A.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLocalDateTime_AsTime(t *testing.T) {
|
|
||||||
d := toml.LocalDateTime{
|
|
||||||
toml.LocalDate{2021, 6, 8},
|
|
||||||
toml.LocalTime{20, 12, 1, 2, 9},
|
|
||||||
}
|
}
|
||||||
cast := d.AsTime(time.UTC)
|
|
||||||
require.Equal(t, time.Date(2021, time.June, 8, 20, 12, 1, 2, time.UTC), cast)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLocalDateTime_String(t *testing.T) {
|
func TestDateIsValid(t *testing.T) {
|
||||||
d := toml.LocalDateTime{
|
for _, test := range []struct {
|
||||||
toml.LocalDate{2021, 6, 8},
|
date LocalDate
|
||||||
toml.LocalTime{20, 12, 1, 2, 9},
|
want bool
|
||||||
|
}{
|
||||||
|
{LocalDate{2014, 7, 29}, true},
|
||||||
|
{LocalDate{2000, 2, 29}, true},
|
||||||
|
{LocalDate{10000, 12, 31}, true},
|
||||||
|
{LocalDate{1, 1, 1}, true},
|
||||||
|
{LocalDate{0, 1, 1}, true}, // year zero is OK
|
||||||
|
{LocalDate{-1, 1, 1}, true}, // negative year is OK
|
||||||
|
{LocalDate{1, 0, 1}, false},
|
||||||
|
{LocalDate{1, 1, 0}, false},
|
||||||
|
{LocalDate{2016, 1, 32}, false},
|
||||||
|
{LocalDate{2016, 13, 1}, false},
|
||||||
|
{LocalDate{1, -1, 1}, false},
|
||||||
|
{LocalDate{1, 1, -1}, false},
|
||||||
|
} {
|
||||||
|
got := test.date.IsValid()
|
||||||
|
if got != test.want {
|
||||||
|
t.Errorf("%#v: got %t, want %t", test.date, got, test.want)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
require.Equal(t, "2021-06-08T20:12:01.000000002", d.String())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLocalDateTime_MarshalText(t *testing.T) {
|
func TestParseDate(t *testing.T) {
|
||||||
d := toml.LocalDateTime{
|
for _, test := range []struct {
|
||||||
toml.LocalDate{2021, 6, 8},
|
str string
|
||||||
toml.LocalTime{20, 12, 1, 2, 9},
|
want LocalDate // if empty, expect an error
|
||||||
|
}{
|
||||||
|
{"2016-01-02", LocalDate{2016, 1, 2}},
|
||||||
|
{"2016-12-31", LocalDate{2016, 12, 31}},
|
||||||
|
{"0003-02-04", LocalDate{3, 2, 4}},
|
||||||
|
{"999-01-26", LocalDate{}},
|
||||||
|
{"", LocalDate{}},
|
||||||
|
{"2016-01-02x", LocalDate{}},
|
||||||
|
} {
|
||||||
|
got, err := ParseLocalDate(test.str)
|
||||||
|
if got != test.want {
|
||||||
|
t.Errorf("ParseLocalDate(%q) = %+v, want %+v", test.str, got, test.want)
|
||||||
|
}
|
||||||
|
if err != nil && test.want != (LocalDate{}) {
|
||||||
|
t.Errorf("Unexpected error %v from ParseLocalDate(%q)", err, test.str)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
b, err := d.MarshalText()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, []byte("2021-06-08T20:12:01.000000002"), b)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLocalDateTime_UnmarshalMarshalText(t *testing.T) {
|
func TestDateArithmetic(t *testing.T) {
|
||||||
d := toml.LocalDateTime{}
|
for _, test := range []struct {
|
||||||
err := d.UnmarshalText([]byte("2021-06-08 20:12:01.000000002"))
|
desc string
|
||||||
require.NoError(t, err)
|
start LocalDate
|
||||||
require.Equal(t, toml.LocalDateTime{
|
end LocalDate
|
||||||
toml.LocalDate{2021, 6, 8},
|
days int
|
||||||
toml.LocalTime{20, 12, 1, 2, 9},
|
}{
|
||||||
}, d)
|
{
|
||||||
|
desc: "zero days noop",
|
||||||
err = d.UnmarshalText([]byte("what"))
|
start: LocalDate{2014, 5, 9},
|
||||||
require.Error(t, err)
|
end: LocalDate{2014, 5, 9},
|
||||||
|
days: 0,
|
||||||
err = d.UnmarshalText([]byte("2021-06-08 20:12:01.000000002 bad"))
|
},
|
||||||
require.Error(t, err)
|
{
|
||||||
|
desc: "crossing a year boundary",
|
||||||
|
start: LocalDate{2014, 12, 31},
|
||||||
|
end: LocalDate{2015, 1, 1},
|
||||||
|
days: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "negative number of days",
|
||||||
|
start: LocalDate{2015, 1, 1},
|
||||||
|
end: LocalDate{2014, 12, 31},
|
||||||
|
days: -1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "full leap year",
|
||||||
|
start: LocalDate{2004, 1, 1},
|
||||||
|
end: LocalDate{2005, 1, 1},
|
||||||
|
days: 366,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "full non-leap year",
|
||||||
|
start: LocalDate{2001, 1, 1},
|
||||||
|
end: LocalDate{2002, 1, 1},
|
||||||
|
days: 365,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "crossing a leap second",
|
||||||
|
start: LocalDate{1972, 6, 30},
|
||||||
|
end: LocalDate{1972, 7, 1},
|
||||||
|
days: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "dates before the unix epoch",
|
||||||
|
start: LocalDate{101, 1, 1},
|
||||||
|
end: LocalDate{102, 1, 1},
|
||||||
|
days: 365,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
if got := test.start.AddDays(test.days); got != test.end {
|
||||||
|
t.Errorf("[%s] %#v.AddDays(%v) = %#v, want %#v", test.desc, test.start, test.days, got, test.end)
|
||||||
|
}
|
||||||
|
if got := test.end.DaysSince(test.start); got != test.days {
|
||||||
|
t.Errorf("[%s] %#v.Sub(%#v) = %v, want %v", test.desc, test.end, test.start, got, test.days)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateBefore(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
d1, d2 LocalDate
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{LocalDate{2016, 12, 31}, LocalDate{2017, 1, 1}, true},
|
||||||
|
{LocalDate{2016, 1, 1}, LocalDate{2016, 1, 1}, false},
|
||||||
|
{LocalDate{2016, 12, 30}, LocalDate{2016, 12, 31}, true},
|
||||||
|
{LocalDate{2016, 1, 30}, LocalDate{2016, 12, 31}, true},
|
||||||
|
} {
|
||||||
|
if got := test.d1.Before(test.d2); got != test.want {
|
||||||
|
t.Errorf("%v.Before(%v): got %t, want %t", test.d1, test.d2, got, test.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateAfter(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
d1, d2 LocalDate
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{LocalDate{2016, 12, 31}, LocalDate{2017, 1, 1}, false},
|
||||||
|
{LocalDate{2016, 1, 1}, LocalDate{2016, 1, 1}, false},
|
||||||
|
{LocalDate{2016, 12, 30}, LocalDate{2016, 12, 31}, false},
|
||||||
|
} {
|
||||||
|
if got := test.d1.After(test.d2); got != test.want {
|
||||||
|
t.Errorf("%v.After(%v): got %t, want %t", test.d1, test.d2, got, test.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeToString(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
str string
|
||||||
|
time LocalTime
|
||||||
|
roundTrip bool // ParseLocalTime(str).String() == str?
|
||||||
|
}{
|
||||||
|
{"13:26:33", LocalTime{13, 26, 33, 0}, true},
|
||||||
|
{"01:02:03.000023456", LocalTime{1, 2, 3, 23456}, true},
|
||||||
|
{"00:00:00.000000001", LocalTime{0, 0, 0, 1}, true},
|
||||||
|
{"13:26:03.1", LocalTime{13, 26, 3, 100000000}, false},
|
||||||
|
{"13:26:33.0000003", LocalTime{13, 26, 33, 300}, false},
|
||||||
|
} {
|
||||||
|
gotTime, err := ParseLocalTime(test.str)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("ParseLocalTime(%q): got error: %v", test.str, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if gotTime != test.time {
|
||||||
|
t.Errorf("ParseLocalTime(%q) = %+v, want %+v", test.str, gotTime, test.time)
|
||||||
|
}
|
||||||
|
if test.roundTrip {
|
||||||
|
gotStr := test.time.String()
|
||||||
|
if gotStr != test.str {
|
||||||
|
t.Errorf("%#v.String() = %q, want %q", test.time, gotStr, test.str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeOf(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
time time.Time
|
||||||
|
want LocalTime
|
||||||
|
}{
|
||||||
|
{time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local), LocalTime{15, 8, 43, 1}},
|
||||||
|
{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC), LocalTime{0, 0, 0, 0}},
|
||||||
|
} {
|
||||||
|
if got := LocalTimeOf(test.time); got != test.want {
|
||||||
|
t.Errorf("LocalTimeOf(%v) = %+v, want %+v", test.time, got, test.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeIsValid(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
time LocalTime
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{LocalTime{0, 0, 0, 0}, true},
|
||||||
|
{LocalTime{23, 0, 0, 0}, true},
|
||||||
|
{LocalTime{23, 59, 59, 999999999}, true},
|
||||||
|
{LocalTime{24, 59, 59, 999999999}, false},
|
||||||
|
{LocalTime{23, 60, 59, 999999999}, false},
|
||||||
|
{LocalTime{23, 59, 60, 999999999}, false},
|
||||||
|
{LocalTime{23, 59, 59, 1000000000}, false},
|
||||||
|
{LocalTime{-1, 0, 0, 0}, false},
|
||||||
|
{LocalTime{0, -1, 0, 0}, false},
|
||||||
|
{LocalTime{0, 0, -1, 0}, false},
|
||||||
|
{LocalTime{0, 0, 0, -1}, false},
|
||||||
|
} {
|
||||||
|
got := test.time.IsValid()
|
||||||
|
if got != test.want {
|
||||||
|
t.Errorf("%#v: got %t, want %t", test.time, got, test.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateTimeToString(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
str string
|
||||||
|
dateTime LocalDateTime
|
||||||
|
roundTrip bool // ParseLocalDateTime(str).String() == str?
|
||||||
|
}{
|
||||||
|
{"2016-03-22T13:26:33", LocalDateTime{LocalDate{2016, 03, 22}, LocalTime{13, 26, 33, 0}}, true},
|
||||||
|
{"2016-03-22T13:26:33.000000600", LocalDateTime{LocalDate{2016, 03, 22}, LocalTime{13, 26, 33, 600}}, true},
|
||||||
|
{"2016-03-22t13:26:33", LocalDateTime{LocalDate{2016, 03, 22}, LocalTime{13, 26, 33, 0}}, false},
|
||||||
|
} {
|
||||||
|
gotDateTime, err := ParseLocalDateTime(test.str)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("ParseLocalDateTime(%q): got error: %v", test.str, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if gotDateTime != test.dateTime {
|
||||||
|
t.Errorf("ParseLocalDateTime(%q) = %+v, want %+v", test.str, gotDateTime, test.dateTime)
|
||||||
|
}
|
||||||
|
if test.roundTrip {
|
||||||
|
gotStr := test.dateTime.String()
|
||||||
|
if gotStr != test.str {
|
||||||
|
t.Errorf("%#v.String() = %q, want %q", test.dateTime, gotStr, test.str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseDateTimeErrors(t *testing.T) {
|
||||||
|
for _, str := range []string{
|
||||||
|
"",
|
||||||
|
"2016-03-22", // just a date
|
||||||
|
"13:26:33", // just a time
|
||||||
|
"2016-03-22 13:26:33", // wrong separating character
|
||||||
|
"2016-03-22T13:26:33x", // extra at end
|
||||||
|
} {
|
||||||
|
if _, err := ParseLocalDateTime(str); err == nil {
|
||||||
|
t.Errorf("ParseLocalDateTime(%q) succeeded, want error", str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateTimeOf(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
time time.Time
|
||||||
|
want LocalDateTime
|
||||||
|
}{
|
||||||
|
{time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local),
|
||||||
|
LocalDateTime{LocalDate{2014, 8, 20}, LocalTime{15, 8, 43, 1}}},
|
||||||
|
{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||||
|
LocalDateTime{LocalDate{1, 1, 1}, LocalTime{0, 0, 0, 0}}},
|
||||||
|
} {
|
||||||
|
if got := LocalDateTimeOf(test.time); got != test.want {
|
||||||
|
t.Errorf("LocalDateTimeOf(%v) = %+v, want %+v", test.time, got, test.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateTimeIsValid(t *testing.T) {
|
||||||
|
// No need to be exhaustive here; it's just LocalDate.IsValid && LocalTime.IsValid.
|
||||||
|
for _, test := range []struct {
|
||||||
|
dt LocalDateTime
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{LocalDateTime{LocalDate{2016, 3, 20}, LocalTime{0, 0, 0, 0}}, true},
|
||||||
|
{LocalDateTime{LocalDate{2016, -3, 20}, LocalTime{0, 0, 0, 0}}, false},
|
||||||
|
{LocalDateTime{LocalDate{2016, 3, 20}, LocalTime{24, 0, 0, 0}}, false},
|
||||||
|
} {
|
||||||
|
got := test.dt.IsValid()
|
||||||
|
if got != test.want {
|
||||||
|
t.Errorf("%#v: got %t, want %t", test.dt, got, test.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateTimeIn(t *testing.T) {
|
||||||
|
dt := LocalDateTime{LocalDate{2016, 1, 2}, LocalTime{3, 4, 5, 6}}
|
||||||
|
got := dt.In(time.UTC)
|
||||||
|
want := time.Date(2016, 1, 2, 3, 4, 5, 6, time.UTC)
|
||||||
|
if !got.Equal(want) {
|
||||||
|
t.Errorf("got %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateTimeBefore(t *testing.T) {
|
||||||
|
d1 := LocalDate{2016, 12, 31}
|
||||||
|
d2 := LocalDate{2017, 1, 1}
|
||||||
|
t1 := LocalTime{5, 6, 7, 8}
|
||||||
|
t2 := LocalTime{5, 6, 7, 9}
|
||||||
|
for _, test := range []struct {
|
||||||
|
dt1, dt2 LocalDateTime
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{LocalDateTime{d1, t1}, LocalDateTime{d2, t1}, true},
|
||||||
|
{LocalDateTime{d1, t1}, LocalDateTime{d1, t2}, true},
|
||||||
|
{LocalDateTime{d2, t1}, LocalDateTime{d1, t1}, false},
|
||||||
|
{LocalDateTime{d2, t1}, LocalDateTime{d2, t1}, false},
|
||||||
|
} {
|
||||||
|
if got := test.dt1.Before(test.dt2); got != test.want {
|
||||||
|
t.Errorf("%v.Before(%v): got %t, want %t", test.dt1, test.dt2, got, test.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateTimeAfter(t *testing.T) {
|
||||||
|
d1 := LocalDate{2016, 12, 31}
|
||||||
|
d2 := LocalDate{2017, 1, 1}
|
||||||
|
t1 := LocalTime{5, 6, 7, 8}
|
||||||
|
t2 := LocalTime{5, 6, 7, 9}
|
||||||
|
for _, test := range []struct {
|
||||||
|
dt1, dt2 LocalDateTime
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{LocalDateTime{d1, t1}, LocalDateTime{d2, t1}, false},
|
||||||
|
{LocalDateTime{d1, t1}, LocalDateTime{d1, t2}, false},
|
||||||
|
{LocalDateTime{d2, t1}, LocalDateTime{d1, t1}, true},
|
||||||
|
{LocalDateTime{d2, t1}, LocalDateTime{d2, t1}, false},
|
||||||
|
} {
|
||||||
|
if got := test.dt1.After(test.dt2); got != test.want {
|
||||||
|
t.Errorf("%v.After(%v): got %t, want %t", test.dt1, test.dt2, got, test.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalJSON(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
value interface{}
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{LocalDate{1987, 4, 15}, `"1987-04-15"`},
|
||||||
|
{LocalTime{18, 54, 2, 0}, `"18:54:02"`},
|
||||||
|
{LocalDateTime{LocalDate{1987, 4, 15}, LocalTime{18, 54, 2, 0}}, `"1987-04-15T18:54:02"`},
|
||||||