Compare commits
270 Commits
v1.9.4
...
specialize
| Author | SHA1 | Date | |
|---|---|---|---|
| dc72d75f3e | |||
| f77775b59e | |||
| b52f6c9823 | |||
| 12244064bb | |||
| 6430ee0bfa | |||
| cf530eba46 | |||
| 64fe47161f | |||
| 4dff8eaa4d | |||
| 2dbd29a565 | |||
| f27a07d31a | |||
| 644515958c | |||
| 8683be35f6 | |||
| dc1740d473 | |||
| 11f789ef11 | |||
| 74d21b367f | |||
| 6617e7e73d | |||
| 3dbca20bc9 | |||
| 85c0658984 | |||
| 772d169b52 | |||
| b4ec220f7e | |||
| 3694ae88f6 | |||
| 19751e8a51 | |||
| 925f214125 | |||
| 39f893ad99 | |||
| c871a61015 | |||
| d0d001625c | |||
| 64941b99e2 | |||
| ed02a1f192 | |||
| 4d7c9ddac7 | |||
| feb1830dcc | |||
| 1c33d6ce20 | |||
| 3000471a12 | |||
| 1f33a6a476 | |||
| 2700aad5d2 | |||
| 7ccaa2744e | |||
| df4bb061f8 | |||
| 9e81ce1c33 | |||
| a23850f29b | |||
| 76f53c857b | |||
| 85f5d567e4 | |||
| bd5cba0b0b | |||
| cd54472d03 | |||
| cc0d1a90ff | |||
| 4984dcb5e9 | |||
| 86632bc190 | |||
| d25eec183f | |||
| e96746311c | |||
| 62acca2b68 | |||
| 476492a85c | |||
| ee9b902222 | |||
| fa56f48daf | |||
| f34c9c332f | |||
| a0d685d482 | |||
| 4a5ae9e81e | |||
| 7e2fa1bc80 | |||
| 40cfb6f458 | |||
| 1230ca485e | |||
| 69ab7e10d1 | |||
| fa07960695 | |||
| 8be357dfa1 | |||
| a93b34d984 | |||
| 9c24fbeaad | |||
| f6b38c33b7 | |||
| 773f10110c | |||
| 618f0181ac | |||
| f3bb20ea79 | |||
| b0d6c62255 | |||
| b202375414 | |||
| 250e073408 | |||
| 11f022ab09 | |||
| 840df4a229 | |||
| c2d1fd86e5 | |||
| 238a6fef7d | |||
| 67852cf007 | |||
| d276c42adc | |||
| 95c701b253 | |||
| 3db329a512 | |||
| 45ea20024b | |||
| ea225df3ed | |||
| 4545a3e94b | |||
| 3f2bb0b363 | |||
| 201d5dd422 | |||
| 1e80267558 | |||
| 931f02a519 | |||
| a533331aee | |||
| 466faaab9f | |||
| e443b4fdb8 | |||
| 2b1c52dddd | |||
| 21445f5170 | |||
| 9ba52996d8 | |||
| 6fe332a869 | |||
| 32c1a8d372 | |||
| ee102a3528 | |||
| 9b67e40640 | |||
| dca2103910 | |||
| a713a96e69 | |||
| a7b50eb8f1 | |||
| 24b62ebe61 | |||
| 9bc4641a49 | |||
| b86b890b8d | |||
| 080baa8574 | |||
| 0537b928df | |||
| 2eff2d082a | |||
| 59cddbc573 | |||
| 9e122af5fc | |||
| ed1f9ed9de | |||
| 466bfe8664 | |||
| e1f035461b | |||
| 84f9e9bceb | |||
| ca41df4a59 | |||
| f2378983d9 | |||
| 37714006b6 | |||
| 275e366c17 | |||
| 18af62d3ea | |||
| af00765ca0 | |||
| 5f877c52fd | |||
| 92b16cad91 | |||
| 4a4c2c2a5f | |||
| 5d905981cf | |||
| 7ccacf158e | |||
| 739ceda96c | |||
| 32da85ab11 | |||
| 18d45c446b | |||
| bcd5333b03 | |||
| e5255a5be2 | |||
| cf288a51c5 | |||
| 72a1afdcb2 | |||
| 2714786b37 | |||
| 51d78a5f0c | |||
| 78389c641a | |||
| c3fc668f27 | |||
| 7f016efe03 | |||
| 269b742eb2 | |||
| 7d8ea80dc3 | |||
| 6165b9454f | |||
| 2ddbf6be6d | |||
| da21b0aecf | |||
| 829c005784 | |||
| b24eb93e8e | |||
| 7dc5550057 | |||
| 9a436c7eeb | |||
| 72c999ecbf | |||
| e5a091a092 | |||
| 317b36b24b | |||
| 636a75f316 | |||
| 390927a0cd | |||
| 3f23ab97e0 | |||
| 47611ff9ea | |||
| f4ac7f7bfa | |||
| e75f23188d | |||
| 6c8adbcb17 | |||
| ffc7d3ba6e | |||
| 4efec6b76a | |||
| 0fcf06e374 | |||
| 1d332cd112 | |||
| 9d3a912da0 | |||
| 1da2fc7e28 | |||
| 17299c937b | |||
| 1bae751a45 | |||
| 8a8d1233bb | |||
| ad538d97c9 | |||
| 43fc2fa552 | |||
| dd5837651d | |||
| a0d031abec | |||
| a25f636a07 | |||
| a3b7e1e353 | |||
| bfeb32c9ce | |||
| 0703eeb262 | |||
| d458ddf4d4 | |||
| 4038ec3dae | |||
| 5b92184e42 | |||
| c6f117c45d | |||
| e78ccff9a4 | |||
| b8da9d1854 | |||
| e5d63aa8fc | |||
| ac2d6e2030 | |||
| fcc91f2618 | |||
| 8b34e54764 | |||
| ebffe6db83 | |||
| 9ec4e86883 | |||
| 93a7b0d77d | |||
| 3e8b8db786 | |||
| 8957a768ef | |||
| fad86a5f24 | |||
| 548b128e67 | |||
| a577df2dbb | |||
| cb678e6221 | |||
| 939f889666 | |||
| f9f9ccb777 | |||
| c6892fcf5a | |||
| 844c9093a2 | |||
| 37d06dabcf | |||
| 1718142ede | |||
| ad64e5d2e2 | |||
| 00b2f776a9 | |||
| b8df31de84 | |||
| 16a336b4f3 | |||
| 590d674153 | |||
| 9a1cfcdd8e | |||
| 590d7faf65 | |||
| de035f0fed | |||
| 04925e4882 | |||
| 3760527218 | |||
| fa7ee6461a | |||
| fbf01f7683 | |||
| a0548e793c | |||
| 1fafb71fd9 | |||
| d8be04d4a8 | |||
| 21d3e85fcc | |||
| 93a74fca35 | |||
| a1c9b661b4 | |||
| 87b9d1cf98 | |||
| 90f3b658c6 | |||
| c35bcc5519 | |||
| f698c102c7 | |||
| 2cee819ce4 | |||
| bf051f1718 | |||
| c77f1d815c | |||
| d24deebee3 | |||
| 4526154571 | |||
| 978143ce99 | |||
| 7f9822db35 | |||
| 052233e858 | |||
| 629a2475a9 | |||
| 46573551f1 | |||
| 1f41c556e8 | |||
| 9ac08febd2 | |||
| 2341b4df00 | |||
| 6e79ce63c2 | |||
| e2a07a3b92 | |||
| 0dad1a950c | |||
| 27f0aeee30 | |||
| 721fa81f2e | |||
| f6a13d6e05 | |||
| 2660bb8426 | |||
| 84282bbfd3 | |||
| 0982fd5f1f | |||
| 7dbf7554c4 | |||
| 2790964270 | |||
| 3488a91eff | |||
| 0e8fd64203 | |||
| 70d41bd750 | |||
| a197513ce7 | |||
| bd8df24646 | |||
| 89052d60b4 | |||
| 9fa2fd413d | |||
| b1e11f82a9 | |||
| 165f65408d | |||
| 540c2a7b59 | |||
| a466f0ca79 | |||
| 736a75748b | |||
| ca12c0670d | |||
| 0ee0fe7f7c | |||
| b123c357c5 | |||
| 94ad175728 | |||
| 1e8b0dc3c9 | |||
| 7300b6a97b | |||
| aae4656c64 | |||
| 44f7a7aead | |||
| bac65cc530 | |||
| 91d7afbc0a | |||
| 7b4d82a939 | |||
| 2ab0f8c733 | |||
| b96c535061 | |||
| fd961100c1 | |||
| 1c7e9fe3af | |||
| 07aa85ea0b | |||
| d54ad15d16 | |||
| abe1005d7a | |||
| b4bb91fc13 |
@@ -0,0 +1,3 @@
|
|||||||
|
* text=auto
|
||||||
|
|
||||||
|
benchmark/benchmark.toml text eol=lf
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<!--
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
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:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 10
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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:
|
||||||
|
- "*"
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master, v2 ]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ master ]
|
||||||
|
schedule:
|
||||||
|
- cron: '26 19 * * 0'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'go' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||||
|
# Learn more:
|
||||||
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
|
# and modify them (or add more) to build your code if your project
|
||||||
|
# uses a compiled language
|
||||||
|
|
||||||
|
#- run: |
|
||||||
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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}"
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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 ./...
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
[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"
|
||||||
|
]
|
||||||
+113
-63
@@ -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 TOML
|
The main goal is the project is to provide an easy-to-use and efficient TOML
|
||||||
implementation for Go that gets the job done and gets out of your way –
|
implementation for Go that gets the job done and gets out of your way – dealing
|
||||||
dealing with TOML is probably not the central piece of your project.
|
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
|
As the single maintainer of go-toml, time is scarce. All help, big or small, is
|
||||||
small, is more than welcomed!
|
more than welcomed!
|
||||||
|
|
||||||
### Ask questions
|
## Ask questions
|
||||||
|
|
||||||
Any question you may have, somebody else might have it too. Always feel
|
Any question you may have, somebody else might have it too. Always feel free to
|
||||||
free to ask them on the [issues tracker][issues-tracker]. We will try to
|
ask them on the [discussion board][discussions]. We will try to answer them as
|
||||||
answer them as clearly and quickly as possible, time permitting.
|
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
|
seemingly innocent question leads to the fix of a bug. Don't hesitate and ask
|
||||||
ask away!
|
away!
|
||||||
|
|
||||||
### Improve the documentation
|
[discussions]: https://github.com/pelletier/go-toml/discussions
|
||||||
|
|
||||||
The best way to share your knowledge and experience with go-toml is to
|
## Improve the documentation
|
||||||
improve the documentation. Fix a typo, clarify an interface, add an
|
|
||||||
example, anything goes!
|
|
||||||
|
|
||||||
The documentation is present in the [README][readme] and thorough the
|
The best way to share your knowledge and experience with go-toml is to improve
|
||||||
source code. On release, it gets updated on [pkg.go.dev][pkg.go.dev]. To make a
|
the documentation. Fix a typo, clarify an interface, add an example, anything
|
||||||
change to the documentation, create a pull request with your proposed
|
goes!
|
||||||
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
|
The documentation is present in the [README][readme] and thorough the source
|
||||||
|
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.
|
||||||
|
|
||||||
Found a bug! Sorry to hear that :(. Help us and other track them down and
|
## Report a bug
|
||||||
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.
|
|
||||||
|
|
||||||
### Code changes
|
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.
|
||||||
|
|
||||||
|
## 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
|
- A short proposal with some POC code is better than a lengthy piece of text
|
||||||
text with no code. Code speaks louder than words.
|
with no code. Code speaks louder than words. That being said, bigger changes
|
||||||
* No backward-incompatible patch will be accepted unless discussed.
|
should probably start with a [discussion][discussions].
|
||||||
Sometimes it's hard, and Go's lack of versioning by default does not
|
- No backward-incompatible patch will be accepted unless discussed. Sometimes
|
||||||
help, but we try not to break people's programs unless we absolutely have
|
it's hard, 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
|
- If you are writing a new feature or extending an existing one, make sure to
|
||||||
to write some documentation.
|
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
|
- Your commit messages need to explain why the change is needed, even if already
|
||||||
already included in the PR description.
|
included in the PR description.
|
||||||
|
|
||||||
It does sound like a lot, but those best practices are here to save time
|
It does sound like a lot, but those best practices are here to save time overall
|
||||||
overall and continuously improve the quality of the project, which is
|
and continuously improve the quality of the project, which is something everyone
|
||||||
something everyone benefits from.
|
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,42 +76,92 @@ 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. You're in!
|
5. Merge.
|
||||||
|
|
||||||
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 ./...`.
|
You can run tests for go-toml using Go's test tool: `go test -race ./...`.
|
||||||
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).
|
|
||||||
|
|
||||||
#### Style
|
During the pull request process, all tests will be ran on Linux, Windows, and
|
||||||
|
MacOS on the last two versions of Go.
|
||||||
|
|
||||||
Try to look around and follow the same format and structure as the rest of
|
However, given GitHub's new policy to _not_ run Actions on pull requests until a
|
||||||
the code. We enforce using `go fmt` on the whole code base.
|
maintainer clicks on button, it is highly recommended that you run them locally
|
||||||
|
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.
|
||||||
* Has relevant unit tests.
|
- Benchstat does not show performance regression.
|
||||||
|
|
||||||
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
@@ -1,11 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
**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,150 +1,426 @@
|
|||||||
# go-toml
|
# go-toml v2
|
||||||
|
|
||||||
Go library for the [TOML](https://toml.io/) format.
|
Go library for the [TOML](https://toml.io/en/) format.
|
||||||
|
|
||||||
This library supports TOML version
|
This library supports [TOML v1.0.0](https://toml.io/en/v1.0.0).
|
||||||
[v1.0.0-rc.3](https://toml.io/en/v1.0.0-rc.3)
|
|
||||||
|
|
||||||
[](https://pkg.go.dev/github.com/pelletier/go-toml)
|
## Development status
|
||||||
[](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)
|
|
||||||
|
|
||||||
## Features
|
This is the upcoming major version of go-toml. It is currently in active
|
||||||
|
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.
|
||||||
|
|
||||||
Go-toml provides the following features for using data parsed from TOML documents:
|
If you do not need the advanced document editing features of v1, you are
|
||||||
|
encouraged to try out this version.
|
||||||
|
|
||||||
* Load TOML documents from files and string data
|
[👉 Roadmap for v2](https://github.com/pelletier/go-toml/discussions/506)
|
||||||
* Easily navigate TOML structure using Tree
|
|
||||||
* Marshaling and unmarshaling to and from data structures
|
[🐞 Bug Reports](https://github.com/pelletier/go-toml/issues)
|
||||||
* Line & column position data for all parsed elements
|
|
||||||
* [Query support similar to JSON-Path](query/)
|
[💬 Anything else](https://github.com/pelletier/go-toml/discussions)
|
||||||
* Syntax errors contain line and column numbers
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Full API, examples, and implementation notes are available in the Go documentation.
|
||||||
|
|
||||||
|
[](https://pkg.go.dev/github.com/pelletier/go-toml/v2)
|
||||||
|
|
||||||
## Import
|
## Import
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "github.com/pelletier/go-toml"
|
import "github.com/pelletier/go-toml/v2"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage example
|
See [Modules](#Modules).
|
||||||
|
|
||||||
Read a TOML document:
|
## Features
|
||||||
|
|
||||||
```go
|
### Stdlib behavior
|
||||||
config, _ := toml.Load(`
|
|
||||||
[postgres]
|
|
||||||
user = "pelletier"
|
|
||||||
password = "mypassword"`)
|
|
||||||
// retrieve data directly
|
|
||||||
user := config.Get("postgres.user").(string)
|
|
||||||
|
|
||||||
// or using an intermediate object
|
As much as possible, this library is designed to behave similarly as the
|
||||||
postgresConfig := config.Get("postgres").(*toml.Tree)
|
standard library's `encoding/json`.
|
||||||
password := postgresConfig.Get("password").(string)
|
|
||||||
|
### 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"
|
||||||
```
|
```
|
||||||
|
|
||||||
Or use Unmarshal:
|
[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 Postgres struct {
|
type MyConfig struct {
|
||||||
User string
|
Version int
|
||||||
Password string
|
Name string
|
||||||
}
|
Tags []string
|
||||||
type Config struct {
|
|
||||||
Postgres Postgres
|
|
||||||
}
|
|
||||||
|
|
||||||
doc := []byte(`
|
|
||||||
[Postgres]
|
|
||||||
User = "pelletier"
|
|
||||||
Password = "mypassword"`)
|
|
||||||
|
|
||||||
config := Config{}
|
|
||||||
toml.Unmarshal(doc, &config)
|
|
||||||
fmt.Println("user=", config.Postgres.User)
|
|
||||||
```
|
|
||||||
|
|
||||||
Or use a query:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// use a query to gather elements without walking the tree
|
|
||||||
q, _ := query.Compile("$..[user,password]")
|
|
||||||
results := q.Execute(config)
|
|
||||||
for ii, item := range results.Values() {
|
|
||||||
fmt.Printf("Query result %d: %v\n", ii, item)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
### Unmarshaling
|
||||||
|
|
||||||
The documentation and additional examples are available at
|
[`Unmarshal`][unmarshal] reads a TOML document and fills a Go structure with its
|
||||||
[pkg.go.dev](https://pkg.go.dev/github.com/pelletier/go-toml).
|
content. For example:
|
||||||
|
|
||||||
## Tools
|
```go
|
||||||
|
doc := `
|
||||||
|
version = 2
|
||||||
|
name = "go-toml"
|
||||||
|
tags = ["go", "toml"]
|
||||||
|
`
|
||||||
|
|
||||||
Go-toml provides three handy command line tools:
|
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)
|
||||||
|
|
||||||
* `tomll`: Reads TOML files and lints them.
|
// Output:
|
||||||
|
// version: 2
|
||||||
```
|
// name: go-toml
|
||||||
go install github.com/pelletier/go-toml/cmd/tomll
|
// tags: [go toml]
|
||||||
tomll --help
|
|
||||||
```
|
|
||||||
* `tomljson`: Reads a TOML file and outputs its JSON representation.
|
|
||||||
|
|
||||||
```
|
|
||||||
go install github.com/pelletier/go-toml/cmd/tomljson
|
|
||||||
tomljson --help
|
|
||||||
```
|
|
||||||
|
|
||||||
* `jsontoml`: Reads a JSON file and outputs a TOML representation.
|
|
||||||
|
|
||||||
```
|
|
||||||
go install github.com/pelletier/go-toml/cmd/jsontoml
|
|
||||||
jsontoml --help
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker image
|
|
||||||
|
|
||||||
Those tools are also availble as a Docker image from
|
|
||||||
[dockerhub](https://hub.docker.com/r/pelletier/go-toml). For example, to
|
|
||||||
use `tomljson`:
|
|
||||||
|
|
||||||
```
|
|
||||||
docker run -v $PWD:/workdir pelletier/go-toml tomljson /workdir/example.toml
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Only master (`latest`) and tagged versions are published to dockerhub. You
|
[unmarshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Unmarshal
|
||||||
can build your own image as usual:
|
|
||||||
|
|
||||||
```
|
### Marshaling
|
||||||
docker build -t go-toml .
|
|
||||||
|
[`Marshal`][marshal] is the opposite of Unmarshal: it represents a Go structure
|
||||||
|
as a TOML document:
|
||||||
|
|
||||||
|
```go
|
||||||
|
cfg := MyConfig{
|
||||||
|
Version: 2,
|
||||||
|
Name: "go-toml",
|
||||||
|
Tags: []string{"go", "toml"},
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := toml.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(b))
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// Version = 2
|
||||||
|
// Name = 'go-toml'
|
||||||
|
// Tags = ['go', 'toml']
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contribute
|
[marshal]: https://pkg.go.dev/github.com/pelletier/go-toml/v2#Marshal
|
||||||
|
|
||||||
Feel free to report bugs and patches using GitHub's pull requests system on
|
## Benchmarks
|
||||||
[pelletier/go-toml](https://github.com/pelletier/go-toml). Any feedback would be
|
|
||||||
much appreciated!
|
|
||||||
|
|
||||||
### Run tests
|
Execution time speedup compared to other Go TOML libraries:
|
||||||
|
|
||||||
`go test ./...`
|
<table>
|
||||||
|
<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>
|
||||||
|
|
||||||
### Fuzzing
|
<table>
|
||||||
|
<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>
|
||||||
|
|
||||||
The script `./fuzz.sh` is available to
|
## Modules
|
||||||
run [go-fuzz](https://github.com/dvyukov/go-fuzz) on go-toml.
|
|
||||||
|
|
||||||
## Versioning
|
go-toml uses Go's standard modules system.
|
||||||
|
|
||||||
Go-toml follows [Semantic Versioning](http://semver.org/). The supported version
|
Installation instructions:
|
||||||
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
|
- Go ≥ 1.16: Nothing to do. Use the import in your code. The `go` command deals
|
||||||
(see [Go Release Policy](https://golang.org/doc/devel/release.html#policy)).
|
with it automatically.
|
||||||
|
- Go ≥ 1.13: `GO111MODULE=on go get github.com/pelletier/go-toml/v2`.
|
||||||
|
|
||||||
|
In case of trouble: [Go Modules FAQ][mod-faq].
|
||||||
|
|
||||||
|
[mod-faq]: https://github.com/golang/go/wiki/Modules#why-does-installing-a-tool-via-go-get-fail-with-error-cannot-find-main-module
|
||||||
|
|
||||||
|
## Migrating from v1
|
||||||
|
|
||||||
|
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.
|
||||||
|
V1 sees that type and uses it when decoding the object.
|
||||||
|
|
||||||
|
When decoding an object into an `interface{}`, V2 instead disregards whatever
|
||||||
|
value the `interface{}` may contain and replaces it with a
|
||||||
|
`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
|
||||||
|
decoder behave like v1.
|
||||||
|
|
||||||
|
#### Values out of array bounds ignored
|
||||||
|
|
||||||
|
When decoding into an array, v1 returns an error when the number of elements
|
||||||
|
contained in the doc is superior to the capacity of the array. For example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
In the same situation, v2 ignores the last value:
|
||||||
|
|
||||||
|
```go
|
||||||
|
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
|
||||||
|
decoder behave like v1.
|
||||||
|
|
||||||
|
#### Support for `toml.Unmarshaler` has been dropped
|
||||||
|
|
||||||
|
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
@@ -0,0 +1,19 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Use this section to tell people about which versions of your project are
|
||||||
|
currently being supported with security updates.
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ---------- | ------------------ |
|
||||||
|
| Latest 2.x | :white_check_mark: |
|
||||||
|
| All 1.x | :x: |
|
||||||
|
| All 0.x | :x: |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Email a vulnerability report to `security@pelletier.codes`. Make sure to include
|
||||||
|
as many details as possible to reproduce the vulnerability. This is a
|
||||||
|
side-project: I will try to get back to you as quickly as possible, time
|
||||||
|
permitting in my personal life. Providing a working patch helps very much!
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
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.15"
|
|
||||||
inputs:
|
|
||||||
version: "1.15"
|
|
||||||
- task: Go@0
|
|
||||||
displayName: "go fmt ./..."
|
|
||||||
inputs:
|
|
||||||
command: 'custom'
|
|
||||||
customCommand: 'fmt'
|
|
||||||
arguments: './...'
|
|
||||||
- job: coverage
|
|
||||||
displayName: "coverage"
|
|
||||||
condition: ne(variables['Build.SourceBranchName'], 'master')
|
|
||||||
pool:
|
|
||||||
vmImage: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- task: GoTool@0
|
|
||||||
displayName: "Install Go 1.15"
|
|
||||||
inputs:
|
|
||||||
version: "1.15"
|
|
||||||
- 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.15"
|
|
||||||
inputs:
|
|
||||||
version: "1.15"
|
|
||||||
- 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.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'
|
|
||||||
linux 1.14:
|
|
||||||
goVersion: '1.14'
|
|
||||||
imageName: 'ubuntu-latest'
|
|
||||||
mac 1.14:
|
|
||||||
goVersion: '1.14'
|
|
||||||
imageName: 'macOS-latest'
|
|
||||||
windows 1.14:
|
|
||||||
goVersion: '1.14'
|
|
||||||
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.15
|
|
||||||
- task: Bash@3
|
|
||||||
inputs:
|
|
||||||
targetType: inline
|
|
||||||
script: "make dist"
|
|
||||||
env:
|
|
||||||
go.goos: $(GOOS)
|
|
||||||
go.goarch: $(GOARCH)
|
|
||||||
- task: CopyFiles@2
|
|
||||||
inputs:
|
|
||||||
sourceFolder: '$(Build.SourcesDirectory)'
|
|
||||||
contents: '*.tar.xz'
|
|
||||||
TargetFolder: '$(Build.ArtifactStagingDirectory)'
|
|
||||||
- task: PublishBuildArtifacts@1
|
|
||||||
inputs:
|
|
||||||
pathtoPublish: '$(Build.ArtifactStagingDirectory)'
|
|
||||||
artifactName: binaries
|
|
||||||
- stage: build_binaries_manifest
|
|
||||||
displayName: "Build binaries manifest"
|
|
||||||
dependsOn: build_binaries
|
|
||||||
jobs:
|
|
||||||
- job: build_manifest
|
|
||||||
displayName: "Build binaries manifest"
|
|
||||||
steps:
|
|
||||||
- task: DownloadBuildArtifacts@0
|
|
||||||
inputs:
|
|
||||||
buildType: 'current'
|
|
||||||
downloadType: 'single'
|
|
||||||
artifactName: 'binaries'
|
|
||||||
downloadPath: '$(Build.SourcesDirectory)'
|
|
||||||
- task: Bash@3
|
|
||||||
inputs:
|
|
||||||
targetType: inline
|
|
||||||
script: "cd binaries && sha256sum --binary *.tar.xz | tee $(Build.ArtifactStagingDirectory)/sha256sums.txt"
|
|
||||||
- task: PublishBuildArtifacts@1
|
|
||||||
inputs:
|
|
||||||
pathtoPublish: '$(Build.ArtifactStagingDirectory)'
|
|
||||||
artifactName: manifest
|
|
||||||
|
|
||||||
- stage: build_docker_image
|
|
||||||
displayName: "Build Docker image"
|
|
||||||
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'
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
#!/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}
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
{
|
|
||||||
"array": {
|
|
||||||
"key1": [
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
3
|
|
||||||
],
|
|
||||||
"key2": [
|
|
||||||
"red",
|
|
||||||
"yellow",
|
|
||||||
"green"
|
|
||||||
],
|
|
||||||
"key3": [
|
|
||||||
[
|
|
||||||
1,
|
|
||||||
2
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3,
|
|
||||||
4,
|
|
||||||
5
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"key4": [
|
|
||||||
[
|
|
||||||
1,
|
|
||||||
2
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"a",
|
|
||||||
"b",
|
|
||||||
"c"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"key5": [
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
3
|
|
||||||
],
|
|
||||||
"key6": [
|
|
||||||
1,
|
|
||||||
2
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"boolean": {
|
|
||||||
"False": false,
|
|
||||||
"True": true
|
|
||||||
},
|
|
||||||
"datetime": {
|
|
||||||
"key1": "1979-05-27T07:32:00Z",
|
|
||||||
"key2": "1979-05-27T00:32:00-07:00",
|
|
||||||
"key3": "1979-05-27T00:32:00.999999-07:00"
|
|
||||||
},
|
|
||||||
"float": {
|
|
||||||
"both": {
|
|
||||||
"key": 6.626e-34
|
|
||||||
},
|
|
||||||
"exponent": {
|
|
||||||
"key1": 5e+22,
|
|
||||||
"key2": 1000000,
|
|
||||||
"key3": -0.02
|
|
||||||
},
|
|
||||||
"fractional": {
|
|
||||||
"key1": 1,
|
|
||||||
"key2": 3.1415,
|
|
||||||
"key3": -0.01
|
|
||||||
},
|
|
||||||
"underscores": {
|
|
||||||
"key1": 9224617.445991227,
|
|
||||||
"key2": 1e+100
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fruit": [{
|
|
||||||
"name": "apple",
|
|
||||||
"physical": {
|
|
||||||
"color": "red",
|
|
||||||
"shape": "round"
|
|
||||||
},
|
|
||||||
"variety": [{
|
|
||||||
"name": "red delicious"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "granny smith"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "banana",
|
|
||||||
"variety": [{
|
|
||||||
"name": "plantain"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"integer": {
|
|
||||||
"key1": 99,
|
|
||||||
"key2": 42,
|
|
||||||
"key3": 0,
|
|
||||||
"key4": -17,
|
|
||||||
"underscores": {
|
|
||||||
"key1": 1000,
|
|
||||||
"key2": 5349221,
|
|
||||||
"key3": 12345
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"products": [{
|
|
||||||
"name": "Hammer",
|
|
||||||
"sku": 738594937
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
"color": "gray",
|
|
||||||
"name": "Nail",
|
|
||||||
"sku": 284758393
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"string": {
|
|
||||||
"basic": {
|
|
||||||
"basic": "I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF."
|
|
||||||
},
|
|
||||||
"literal": {
|
|
||||||
"multiline": {
|
|
||||||
"lines": "The first newline is\ntrimmed in raw strings.\n All other whitespace\n is preserved.\n",
|
|
||||||
"regex2": "I [dw]on't need \\d{2} apples"
|
|
||||||
},
|
|
||||||
"quoted": "Tom \"Dubs\" Preston-Werner",
|
|
||||||
"regex": "\u003c\\i\\c*\\s*\u003e",
|
|
||||||
"winpath": "C:\\Users\\nodejs\\templates",
|
|
||||||
"winpath2": "\\\\ServerX\\admin$\\system32\\"
|
|
||||||
},
|
|
||||||
"multiline": {
|
|
||||||
"continued": {
|
|
||||||
"key1": "The quick brown fox jumps over the lazy dog.",
|
|
||||||
"key2": "The quick brown fox jumps over the lazy dog.",
|
|
||||||
"key3": "The quick brown fox jumps over the lazy dog."
|
|
||||||
},
|
|
||||||
"key1": "One\nTwo",
|
|
||||||
"key2": "One\nTwo",
|
|
||||||
"key3": "One\nTwo"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"table": {
|
|
||||||
"inline": {
|
|
||||||
"name": {
|
|
||||||
"first": "Tom",
|
|
||||||
"last": "Preston-Werner"
|
|
||||||
},
|
|
||||||
"point": {
|
|
||||||
"x": 1,
|
|
||||||
"y": 2
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"key": "value",
|
|
||||||
"subtable": {
|
|
||||||
"key": "another value"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x": {
|
|
||||||
"y": {
|
|
||||||
"z": {
|
|
||||||
"w": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
---
|
|
||||||
array:
|
|
||||||
key1:
|
|
||||||
- 1
|
|
||||||
- 2
|
|
||||||
- 3
|
|
||||||
key2:
|
|
||||||
- red
|
|
||||||
- yellow
|
|
||||||
- green
|
|
||||||
key3:
|
|
||||||
- - 1
|
|
||||||
- 2
|
|
||||||
- - 3
|
|
||||||
- 4
|
|
||||||
- 5
|
|
||||||
key4:
|
|
||||||
- - 1
|
|
||||||
- 2
|
|
||||||
- - a
|
|
||||||
- b
|
|
||||||
- c
|
|
||||||
key5:
|
|
||||||
- 1
|
|
||||||
- 2
|
|
||||||
- 3
|
|
||||||
key6:
|
|
||||||
- 1
|
|
||||||
- 2
|
|
||||||
boolean:
|
|
||||||
'False': false
|
|
||||||
'True': true
|
|
||||||
datetime:
|
|
||||||
key1: '1979-05-27T07:32:00Z'
|
|
||||||
key2: '1979-05-27T00:32:00-07:00'
|
|
||||||
key3: '1979-05-27T00:32:00.999999-07:00'
|
|
||||||
float:
|
|
||||||
both:
|
|
||||||
key: 6.626e-34
|
|
||||||
exponent:
|
|
||||||
key1: 5.0e+22
|
|
||||||
key2: 1000000
|
|
||||||
key3: -0.02
|
|
||||||
fractional:
|
|
||||||
key1: 1
|
|
||||||
key2: 3.1415
|
|
||||||
key3: -0.01
|
|
||||||
underscores:
|
|
||||||
key1: 9224617.445991227
|
|
||||||
key2: 1.0e+100
|
|
||||||
fruit:
|
|
||||||
- name: apple
|
|
||||||
physical:
|
|
||||||
color: red
|
|
||||||
shape: round
|
|
||||||
variety:
|
|
||||||
- name: red delicious
|
|
||||||
- name: granny smith
|
|
||||||
- name: banana
|
|
||||||
variety:
|
|
||||||
- name: plantain
|
|
||||||
integer:
|
|
||||||
key1: 99
|
|
||||||
key2: 42
|
|
||||||
key3: 0
|
|
||||||
key4: -17
|
|
||||||
underscores:
|
|
||||||
key1: 1000
|
|
||||||
key2: 5349221
|
|
||||||
key3: 12345
|
|
||||||
products:
|
|
||||||
- name: Hammer
|
|
||||||
sku: 738594937
|
|
||||||
- {}
|
|
||||||
- color: gray
|
|
||||||
name: Nail
|
|
||||||
sku: 284758393
|
|
||||||
string:
|
|
||||||
basic:
|
|
||||||
basic: "I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF."
|
|
||||||
literal:
|
|
||||||
multiline:
|
|
||||||
lines: |
|
|
||||||
The first newline is
|
|
||||||
trimmed in raw strings.
|
|
||||||
All other whitespace
|
|
||||||
is preserved.
|
|
||||||
regex2: I [dw]on't need \d{2} apples
|
|
||||||
quoted: Tom "Dubs" Preston-Werner
|
|
||||||
regex: "<\\i\\c*\\s*>"
|
|
||||||
winpath: C:\Users\nodejs\templates
|
|
||||||
winpath2: "\\\\ServerX\\admin$\\system32\\"
|
|
||||||
multiline:
|
|
||||||
continued:
|
|
||||||
key1: The quick brown fox jumps over the lazy dog.
|
|
||||||
key2: The quick brown fox jumps over the lazy dog.
|
|
||||||
key3: The quick brown fox jumps over the lazy dog.
|
|
||||||
key1: |-
|
|
||||||
One
|
|
||||||
Two
|
|
||||||
key2: |-
|
|
||||||
One
|
|
||||||
Two
|
|
||||||
key3: |-
|
|
||||||
One
|
|
||||||
Two
|
|
||||||
table:
|
|
||||||
inline:
|
|
||||||
name:
|
|
||||||
first: Tom
|
|
||||||
last: Preston-Werner
|
|
||||||
point:
|
|
||||||
x: 1
|
|
||||||
y: 2
|
|
||||||
key: value
|
|
||||||
subtable:
|
|
||||||
key: another value
|
|
||||||
x:
|
|
||||||
y:
|
|
||||||
z:
|
|
||||||
w: {}
|
|
||||||
+543
-84
@@ -1,17 +1,241 @@
|
|||||||
package benchmark
|
package benchmark_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
burntsushi "github.com/BurntSushi/toml"
|
"github.com/pelletier/go-toml/v2"
|
||||||
"github.com/pelletier/go-toml"
|
"github.com/stretchr/testify/require"
|
||||||
"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
|
||||||
@@ -25,7 +249,7 @@ type benchmarkDoc struct {
|
|||||||
}
|
}
|
||||||
Point struct {
|
Point struct {
|
||||||
X int64
|
X int64
|
||||||
U int64
|
Y int64
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,7 +321,7 @@ type benchmarkDoc struct {
|
|||||||
Key1 []int64
|
Key1 []int64
|
||||||
Key2 []string
|
Key2 []string
|
||||||
Key3 [][]int64
|
Key3 [][]int64
|
||||||
// TODO: Key4 not supported by go-toml's Unmarshal
|
Key4 []interface{}
|
||||||
Key5 []int64
|
Key5 []int64
|
||||||
Key6 []int64
|
Key6 []int64
|
||||||
}
|
}
|
||||||
@@ -109,86 +333,321 @@ 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 {
|
}
|
||||||
Name string
|
Variety []struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalReferenceFile(t *testing.T) {
|
||||||
|
bytes, err := ioutil.ReadFile("benchmark.toml")
|
||||||
|
require.NoError(t, err)
|
||||||
|
d := benchmarkDoc{}
|
||||||
|
err = toml.Unmarshal(bytes, &d)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := benchmarkDoc{
|
||||||
|
Table: struct {
|
||||||
|
Key string
|
||||||
|
Subtable struct{ Key string }
|
||||||
|
Inline struct {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkParseToml(b *testing.B) {
|
var hugoFrontMatterbytes = []byte(`
|
||||||
fileBytes, err := ioutil.ReadFile("benchmark.toml")
|
categories = ["Development", "VIM"]
|
||||||
if err != nil {
|
date = "2012-04-06"
|
||||||
b.Fatal(err)
|
description = "spf13-vim is a cross platform distribution of vim plugins and resources for Vim."
|
||||||
}
|
slug = "spf13-vim-3-0-release-and-new-website"
|
||||||
b.ResetTimer()
|
tags = [".vimrc", "plugins", "spf13-vim", "vim"]
|
||||||
for i := 0; i < b.N; i++ {
|
title = "spf13-vim 3.0 release and new website"
|
||||||
_, err := toml.LoadReader(bytes.NewReader(fileBytes))
|
include_toc = true
|
||||||
if err != nil {
|
show_comments = false
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkUnmarshalToml(b *testing.B) {
|
[[cascade]]
|
||||||
bytes, err := ioutil.ReadFile("benchmark.toml")
|
background = "yosemite.jpg"
|
||||||
if err != nil {
|
[cascade._target]
|
||||||
b.Fatal(err)
|
kind = "page"
|
||||||
}
|
lang = "en"
|
||||||
b.ReportAllocs()
|
path = "/blog/**"
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
target := benchmarkDoc{}
|
|
||||||
err := toml.Unmarshal(bytes, &target)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkUnmarshalBurntSushiToml(b *testing.B) {
|
[[cascade]]
|
||||||
bytes, err := ioutil.ReadFile("benchmark.toml")
|
background = "goldenbridge.jpg"
|
||||||
if err != nil {
|
[cascade._target]
|
||||||
b.Fatal(err)
|
kind = "section"
|
||||||
}
|
`)
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
target := benchmarkDoc{}
|
|
||||||
err := burntsushi.Unmarshal(bytes, &target)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkUnmarshalJson(b *testing.B) {
|
|
||||||
bytes, err := ioutil.ReadFile("benchmark.json")
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
target := benchmarkDoc{}
|
|
||||||
err := json.Unmarshal(bytes, &target)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkUnmarshalYaml(b *testing.B) {
|
|
||||||
bytes, err := ioutil.ReadFile("benchmark.yml")
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
target := benchmarkDoc{}
|
|
||||||
err := yaml.Unmarshal(bytes, &target)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
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 => ../
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
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.
@@ -0,0 +1,71 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
#!/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
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
+23
-19
@@ -1,8 +1,9 @@
|
|||||||
// Tomltestgen is a program that retrieves a given version of
|
// tomltestgen retrieves a given version of the language-agnostic TOML test suite in
|
||||||
// https://github.com/BurntSushi/toml-test and generates go code for go-toml's unit tests
|
// https://github.com/BurntSushi/toml-test and generates go-toml unit tests.
|
||||||
// based on the test files.
|
|
||||||
//
|
//
|
||||||
// Usage: go run github.com/pelletier/go-toml/cmd/tomltestgen > toml_testgen_test.go
|
// Within the go-toml package, run `go generate`. Otherwise, use:
|
||||||
|
//
|
||||||
|
// go run github.com/pelletier/go-toml/cmd/tomltestgen -o toml_testgen_test.go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -43,20 +44,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\n" +
|
"package toml_test\n" +
|
||||||
" import (\n" +
|
" import (\n" +
|
||||||
" \"testing\"\n" +
|
" \"testing\"\n" +
|
||||||
")\n" +
|
")\n" +
|
||||||
|
|
||||||
"{{range .Invalid}}\n" +
|
"{{range .Invalid}}\n" +
|
||||||
"func TestInvalid{{.Name}}(t *testing.T) {\n" +
|
"func TestTOMLTest_Invalid_{{.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 TestValid{{.Name}}(t *testing.T) {\n" +
|
"func TestTOMLTest_Valid_{{.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" +
|
||||||
@@ -96,6 +97,9 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -117,21 +121,12 @@ func readFileFromZip(f *zip.File) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func templateGoStr(input string) string {
|
func templateGoStr(input string) string {
|
||||||
if len(input) > 0 && input[len(input)-1] == '\n' {
|
return strconv.Quote(input)
|
||||||
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() {
|
||||||
@@ -215,5 +210,14 @@ 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,496 @@
|
|||||||
|
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,23 +1,2 @@
|
|||||||
// Package toml is a TOML parser and manipulation library.
|
// Package toml is a library to read and write TOML documents.
|
||||||
//
|
|
||||||
// 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
@@ -1,170 +0,0 @@
|
|||||||
// 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"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
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
@@ -0,0 +1,226 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# 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
@@ -0,0 +1,100 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
// +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
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
#! /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,5 +1,6 @@
|
|||||||
module github.com/pelletier/go-toml
|
module github.com/pelletier/go-toml/v2
|
||||||
|
|
||||||
go 1.12
|
go 1.16
|
||||||
|
|
||||||
require github.com/davecgh/go-spew v1.1.1
|
// latest (v1.7.0) doesn't have the fix for time.Time
|
||||||
|
require github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942
|
||||||
|
|||||||
@@ -1,2 +1,11 @@
|
|||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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=
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
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()}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
//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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
//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
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
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])
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
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
@@ -0,0 +1,50 @@
|
|||||||
|
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]
|
||||||
|
}
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
package tracker
|
||||||
-112
@@ -1,112 +0,0 @@
|
|||||||
// 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)
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
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
+80
-241
@@ -1,281 +1,120 @@
|
|||||||
// 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A LocalDate represents a date (year, month, day).
|
// LocalDate represents a calendar day in no specific timezone.
|
||||||
//
|
|
||||||
// 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 (e.g., 2014).
|
Year int
|
||||||
Month time.Month // Month of the year (January = 1, ...).
|
Month int
|
||||||
Day int // Day of the month, starting at 1.
|
Day int
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocalDateOf returns the LocalDate in which a time occurs in that time's location.
|
// AsTime converts d into a specific time instance at midnight in zone.
|
||||||
func LocalDateOf(t time.Time) LocalDate {
|
func (d LocalDate) AsTime(zone *time.Location) time.Time {
|
||||||
var d LocalDate
|
return time.Date(d.Year, time.Month(d.Month), d.Day, 0, 0, 0, 0, zone)
|
||||||
d.Year, d.Month, d.Day = t.Date()
|
|
||||||
return d
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseLocalDate parses a string in RFC3339 full-date format and returns the date value it represents.
|
// String returns RFC 3339 representation of d.
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValid reports whether the date is valid.
|
// MarshalText returns RFC 3339 representation of d.
|
||||||
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 implements the encoding.TextUnmarshaler interface.
|
// UnmarshalText parses b using RFC 3339 to fill d.
|
||||||
// The date is expected to be a string in a format accepted by ParseLocalDate.
|
func (d *LocalDate) UnmarshalText(b []byte) error {
|
||||||
func (d *LocalDate) UnmarshalText(data []byte) error {
|
res, err := parseLocalDate(b)
|
||||||
var err error
|
if err != nil {
|
||||||
*d, err = ParseLocalDate(string(data))
|
return err
|
||||||
return err
|
}
|
||||||
|
*d = res
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// A LocalTime represents a time with nanosecond precision.
|
// LocalTime represents a time of day of no specific day in no specific
|
||||||
//
|
// 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 // The hour of the day in 24-hour format; range [0-23]
|
Hour int // Hour of the day: [0; 24[
|
||||||
Minute int // The minute of the hour; range [0-59]
|
Minute int // Minute of the hour: [0; 60[
|
||||||
Second int // The second of the minute; range [0-59]
|
Second int // Second of the minute: [0; 60[
|
||||||
Nanosecond int // The nanosecond of the second; range [0-999999999]
|
Nanosecond int // Nanoseconds within the second: [0, 1000000000[
|
||||||
|
Precision int // Number of digits to display for Nanosecond.
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocalTimeOf returns the LocalTime representing the time of day in which a time occurs
|
// String returns RFC 3339 representation of d.
|
||||||
// in that time's location. It ignores the date.
|
// If d.Nanosecond and d.Precision are zero, the time won't have a nanosecond
|
||||||
func LocalTimeOf(t time.Time) LocalTime {
|
// component. If d.Nanosecond > 0 but d.Precision = 0, then the minimum number
|
||||||
var tm LocalTime
|
// of digits for nanoseconds is provided.
|
||||||
tm.Hour, tm.Minute, tm.Second = t.Clock()
|
func (d LocalTime) String() string {
|
||||||
tm.Nanosecond = t.Nanosecond()
|
s := fmt.Sprintf("%02d:%02d:%02d", d.Hour, d.Minute, d.Second)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseLocalTime parses a string and returns the time value it represents.
|
// MarshalText returns RFC 3339 representation of d.
|
||||||
// ParseLocalTime accepts an extended form of the RFC3339 partial-time format. After
|
func (d LocalTime) MarshalText() ([]byte, error) {
|
||||||
// the HH:MM:SS part of the string, an optional fractional part may appear,
|
return []byte(d.String()), nil
|
||||||
// consisting of a decimal point followed by one to nine decimal digits.
|
}
|
||||||
// (RFC3339 admits only one digit after the decimal point).
|
|
||||||
func ParseLocalTime(s string) (LocalTime, error) {
|
// UnmarshalText parses b using RFC 3339 to fill d.
|
||||||
t, err := time.Parse("15:04:05.999999999", s)
|
func (d *LocalTime) UnmarshalText(b []byte) error {
|
||||||
|
res, left, err := parseLocalTime(b)
|
||||||
|
if err == nil && len(left) != 0 {
|
||||||
|
err = newDecodeError(left, "extra characters")
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return LocalTime{}, err
|
return err
|
||||||
}
|
}
|
||||||
return LocalTimeOf(t), nil
|
*d = res
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the date in the format described in ParseLocalTime. If Nanoseconds
|
// LocalDateTime represents a time of a specific day in no specific timezone.
|
||||||
// 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 {
|
||||||
Date LocalDate
|
LocalDate
|
||||||
Time LocalTime
|
LocalTime
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: We deliberately do not embed LocalDate into LocalDateTime, to avoid promoting AddDays and Sub.
|
// AsTime converts d into a specific time instance in zone.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
// LocalDateTimeOf returns the LocalDateTime in which a time occurs in that time's location.
|
// String returns RFC 3339 representation of d.
|
||||||
func LocalDateTimeOf(t time.Time) LocalDateTime {
|
func (d LocalDateTime) String() string {
|
||||||
return LocalDateTime{
|
return d.LocalDate.String() + "T" + d.LocalTime.String()
|
||||||
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 {
|
||||||
t, err = time.Parse("2006-01-02t15:04:05.999999999", s)
|
return err
|
||||||
if err != nil {
|
|
||||||
return LocalDateTime{}, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return LocalDateTimeOf(t), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the date in the format described in ParseLocalDate.
|
*d = res
|
||||||
func (dt LocalDateTime) String() string {
|
return nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
+86
-414
@@ -1,446 +1,118 @@
|
|||||||
// Copyright 2016 Google LLC
|
package toml_test
|
||||||
//
|
|
||||||
// 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 cmpEqual(x, y interface{}) bool {
|
func TestLocalDate_AsTime(t *testing.T) {
|
||||||
return reflect.DeepEqual(x, y)
|
d := toml.LocalDate{2021, 6, 8}
|
||||||
|
cast := d.AsTime(time.UTC)
|
||||||
|
require.Equal(t, time.Date(2021, time.June, 8, 0, 0, 0, 0, time.UTC), cast)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDates(t *testing.T) {
|
func TestLocalDate_String(t *testing.T) {
|
||||||
for _, test := range []struct {
|
d := toml.LocalDate{2021, 6, 8}
|
||||||
date LocalDate
|
require.Equal(t, "2021-06-08", d.String())
|
||||||
loc *time.Location
|
|
||||||
wantStr string
|
|
||||||
wantTime time.Time
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
date: LocalDate{2014, 7, 29},
|
|
||||||
loc: time.Local,
|
|
||||||
wantStr: "2014-07-29",
|
|
||||||
wantTime: time.Date(2014, time.July, 29, 0, 0, 0, 0, time.Local),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: LocalDateOf(time.Date(2014, 8, 20, 15, 8, 43, 1, time.Local)),
|
|
||||||
loc: time.UTC,
|
|
||||||
wantStr: "2014-08-20",
|
|
||||||
wantTime: time.Date(2014, 8, 20, 0, 0, 0, 0, time.UTC),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: LocalDateOf(time.Date(999, time.January, 26, 0, 0, 0, 0, time.Local)),
|
|
||||||
loc: time.UTC,
|
|
||||||
wantStr: "0999-01-26",
|
|
||||||
wantTime: time.Date(999, 1, 26, 0, 0, 0, 0, time.UTC),
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
if got := test.date.String(); got != test.wantStr {
|
|
||||||
t.Errorf("%#v.String() = %q, want %q", test.date, got, test.wantStr)
|
|
||||||
}
|
|
||||||
if got := test.date.In(test.loc); !got.Equal(test.wantTime) {
|
|
||||||
t.Errorf("%#v.In(%v) = %v, want %v", test.date, test.loc, got, test.wantTime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDateIsValid(t *testing.T) {
|
func TestLocalDate_MarshalText(t *testing.T) {
|
||||||
for _, test := range []struct {
|
d := toml.LocalDate{2021, 6, 8}
|
||||||
date LocalDate
|
b, err := d.MarshalText()
|
||||||
want bool
|
require.NoError(t, err)
|
||||||
}{
|
require.Equal(t, []byte("2021-06-08"), b)
|
||||||
{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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseDate(t *testing.T) {
|
func TestLocalDate_UnmarshalMarshalText(t *testing.T) {
|
||||||
for _, test := range []struct {
|
d := toml.LocalDate{}
|
||||||
str string
|
err := d.UnmarshalText([]byte("2021-06-08"))
|
||||||
want LocalDate // if empty, expect an error
|
require.NoError(t, err)
|
||||||
}{
|
require.Equal(t, toml.LocalDate{2021, 6, 8}, d)
|
||||||
{"2016-01-02", LocalDate{2016, 1, 2}},
|
|
||||||
{"2016-12-31", LocalDate{2016, 12, 31}},
|
err = d.UnmarshalText([]byte("what"))
|
||||||
{"0003-02-04", LocalDate{3, 2, 4}},
|
require.Error(t, err)
|
||||||
{"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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDateArithmetic(t *testing.T) {
|
func TestLocalTime_String(t *testing.T) {
|
||||||
for _, test := range []struct {
|
d := toml.LocalTime{20, 12, 1, 2, 9}
|
||||||
desc string
|
require.Equal(t, "20:12:01.000000002", d.String())
|
||||||
start LocalDate
|
d = toml.LocalTime{20, 12, 1, 0, 0}
|
||||||
end LocalDate
|
require.Equal(t, "20:12:01", d.String())
|
||||||
days int
|
d = toml.LocalTime{20, 12, 1, 0, 9}
|
||||||
}{
|
require.Equal(t, "20:12:01.000000000", d.String())
|
||||||
{
|
d = toml.LocalTime{20, 12, 1, 100, 0}
|
||||||
desc: "zero days noop",
|
require.Equal(t, "20:12:01.0000001", d.String())
|
||||||
start: LocalDate{2014, 5, 9},
|
|
||||||
end: LocalDate{2014, 5, 9},
|
|
||||||
days: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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) {
|
func TestLocalTime_MarshalText(t *testing.T) {
|
||||||
for _, test := range []struct {
|
d := toml.LocalTime{20, 12, 1, 2, 9}
|
||||||
d1, d2 LocalDate
|
b, err := d.MarshalText()
|
||||||
want bool
|
require.NoError(t, err)
|
||||||
}{
|
require.Equal(t, []byte("20:12:01.000000002"), b)
|
||||||
{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) {
|
func TestLocalTime_UnmarshalMarshalText(t *testing.T) {
|
||||||
for _, test := range []struct {
|
d := toml.LocalTime{}
|
||||||
d1, d2 LocalDate
|
err := d.UnmarshalText([]byte("20:12:01.000000002"))
|
||||||
want bool
|
require.NoError(t, err)
|
||||||
}{
|
require.Equal(t, toml.LocalTime{20, 12, 1, 2, 9}, d)
|
||||||
{LocalDate{2016, 12, 31}, LocalDate{2017, 1, 1}, false},
|
|
||||||
{LocalDate{2016, 1, 1}, LocalDate{2016, 1, 1}, false},
|
err = d.UnmarshalText([]byte("what"))
|
||||||
{LocalDate{2016, 12, 30}, LocalDate{2016, 12, 31}, false},
|
require.Error(t, err)
|
||||||
} {
|
|
||||||
if got := test.d1.After(test.d2); got != test.want {
|
err = d.UnmarshalText([]byte("20:12:01.000000002 bad"))
|
||||||
t.Errorf("%v.After(%v): got %t, want %t", test.d1, test.d2, got, test.want)
|
require.Error(t, err)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTimeToString(t *testing.T) {
|
func TestLocalTime_RoundTrip(t *testing.T) {
|
||||||
for _, test := range []struct {
|
var d struct{ A toml.LocalTime }
|
||||||
str string
|
err := toml.Unmarshal([]byte("a=20:12:01.500"), &d)
|
||||||
time LocalTime
|
require.NoError(t, err)
|
||||||
roundTrip bool // ParseLocalTime(str).String() == str?
|
require.Equal(t, "20:12:01.500", d.A.String())
|
||||||
}{
|
|
||||||
{"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) {
|
func TestLocalDateTime_AsTime(t *testing.T) {
|
||||||
for _, test := range []struct {
|
d := toml.LocalDateTime{
|
||||||
time time.Time
|
toml.LocalDate{2021, 6, 8},
|
||||||
want LocalTime
|
toml.LocalTime{20, 12, 1, 2, 9},
|
||||||
}{
|
|
||||||
{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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
cast := d.AsTime(time.UTC)
|
||||||
|
require.Equal(t, time.Date(2021, time.June, 8, 20, 12, 1, 2, time.UTC), cast)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTimeIsValid(t *testing.T) {
|
func TestLocalDateTime_String(t *testing.T) {
|
||||||
for _, test := range []struct {
|
d := toml.LocalDateTime{
|
||||||
time LocalTime
|
toml.LocalDate{2021, 6, 8},
|
||||||
want bool
|
toml.LocalTime{20, 12, 1, 2, 9},
|
||||||
}{
|
|
||||||
{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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
require.Equal(t, "2021-06-08T20:12:01.000000002", d.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDateTimeToString(t *testing.T) {
|
func TestLocalDateTime_MarshalText(t *testing.T) {
|
||||||
for _, test := range []struct {
|
d := toml.LocalDateTime{
|
||||||
str string
|
toml.LocalDate{2021, 6, 8},
|
||||||
dateTime LocalDateTime
|
toml.LocalTime{20, 12, 1, 2, 9},
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
b, err := d.MarshalText()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, []byte("2021-06-08T20:12:01.000000002"), b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseDateTimeErrors(t *testing.T) {
|
func TestLocalDateTime_UnmarshalMarshalText(t *testing.T) {
|
||||||
for _, str := range []string{
|
d := toml.LocalDateTime{}
|
||||||
"",
|
err := d.UnmarshalText([]byte("2021-06-08 20:12:01.000000002"))
|
||||||
"2016-03-22", // just a date
|
require.NoError(t, err)
|
||||||
"13:26:33", // just a time
|
require.Equal(t, toml.LocalDateTime{
|
||||||
"2016-03-22 13:26:33", // wrong separating character
|
toml.LocalDate{2021, 6, 8},
|
||||||
"2016-03-22T13:26:33x", // extra at end
|
toml.LocalTime{20, 12, 1, 2, 9},
|
||||||
} {
|
}, d)
|
||||||
if _, err := ParseLocalDateTime(str); err == nil {
|
|
||||||
t.Errorf("ParseLocalDateTime(%q) succeeded, want error", str)
|
err = d.UnmarshalText([]byte("what"))
|
||||||
}
|
require.Error(t, err)
|
||||||
}
|
|
||||||
}
|
err = d.UnmarshalText([]byte("2021-06-08 20:12:01.000000002 bad"))
|
||||||
|
require.Error(t, err)
|
||||||
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"`},
|
|
||||||
} {
|
|
||||||
bgot, err := json.Marshal(test.value)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if got := string(bgot); got != test.want {
|
|
||||||
t.Errorf("%#v: got %s, want %s", test.value, got, test.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnmarshalJSON(t *testing.T) {
|
|
||||||
var d LocalDate
|
|
||||||
var tm LocalTime
|
|
||||||
var dt LocalDateTime
|
|
||||||
for _, test := range []struct {
|
|
||||||
data string
|
|
||||||
ptr interface{}
|
|
||||||
want interface{}
|
|
||||||
}{
|
|
||||||
{`"1987-04-15"`, &d, &LocalDate{1987, 4, 15}},
|
|
||||||
{`"1987-04-\u0031\u0035"`, &d, &LocalDate{1987, 4, 15}},
|
|
||||||
{`"18:54:02"`, &tm, &LocalTime{18, 54, 2, 0}},
|
|
||||||
{`"1987-04-15T18:54:02"`, &dt, &LocalDateTime{LocalDate{1987, 4, 15}, LocalTime{18, 54, 2, 0}}},
|
|
||||||
} {
|
|
||||||
if err := json.Unmarshal([]byte(test.data), test.ptr); err != nil {
|
|
||||||
t.Fatalf("%s: %v", test.data, err)
|
|
||||||
}
|
|
||||||
if !cmpEqual(test.ptr, test.want) {
|
|
||||||
t.Errorf("%s: got %#v, want %#v", test.data, test.ptr, test.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, bad := range []string{"", `""`, `"bad"`, `"1987-04-15x"`,
|
|
||||||
`19870415`, // a JSON number
|
|
||||||
`11987-04-15x`, // not a JSON string
|
|
||||||
|
|
||||||
} {
|
|
||||||
if json.Unmarshal([]byte(bad), &d) == nil {
|
|
||||||
t.Errorf("%q, LocalDate: got nil, want error", bad)
|
|
||||||
}
|
|
||||||
if json.Unmarshal([]byte(bad), &tm) == nil {
|
|
||||||
t.Errorf("%q, LocalTime: got nil, want error", bad)
|
|
||||||
}
|
|
||||||
if json.Unmarshal([]byte(bad), &dt) == nil {
|
|
||||||
t.Errorf("%q, LocalDateTime: got nil, want error", bad)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
-1293
File diff suppressed because it is too large
Load Diff
@@ -1,39 +0,0 @@
|
|||||||
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"
|
|
||||||
-4054
File diff suppressed because it is too large
Load Diff
@@ -1,39 +0,0 @@
|
|||||||
title = "TOML Marshal Testing"
|
|
||||||
|
|
||||||
[basic]
|
|
||||||
bool = true
|
|
||||||
date = 1979-05-27T07:32:00Z
|
|
||||||
float = 123.4
|
|
||||||
float64 = 123.456782132399
|
|
||||||
int = 5000
|
|
||||||
string = "Bite me"
|
|
||||||
uint = 5001
|
|
||||||
|
|
||||||
[basic_lists]
|
|
||||||
bools = [true,false,true]
|
|
||||||
dates = [1979-05-27T07:32:00Z,1980-05-27T07:32:00Z]
|
|
||||||
floats = [12.3,45.6,78.9]
|
|
||||||
ints = [8001,8001,8002]
|
|
||||||
strings = ["One","Two","Three"]
|
|
||||||
uints = [5002,5003]
|
|
||||||
|
|
||||||
[basic_map]
|
|
||||||
one = "one"
|
|
||||||
two = "two"
|
|
||||||
|
|
||||||
[subdoc]
|
|
||||||
|
|
||||||
[subdoc.first]
|
|
||||||
name = "First"
|
|
||||||
|
|
||||||
[subdoc.second]
|
|
||||||
name = "Second"
|
|
||||||
|
|
||||||
[[subdoclist]]
|
|
||||||
name = "List.First"
|
|
||||||
|
|
||||||
[[subdoclist]]
|
|
||||||
name = "List.Second"
|
|
||||||
|
|
||||||
[[subdocptrs]]
|
|
||||||
name = "Second"
|
|
||||||
+810
@@ -0,0 +1,810 @@
|
|||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Marshal serializes a Go value as a TOML document.
|
||||||
|
//
|
||||||
|
// It is a shortcut for Encoder.Encode() with the default options.
|
||||||
|
func Marshal(v interface{}) ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
enc := NewEncoder(&buf)
|
||||||
|
|
||||||
|
err := enc.Encode(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encoder writes a TOML document to an output stream.
|
||||||
|
type Encoder struct {
|
||||||
|
// output
|
||||||
|
w io.Writer
|
||||||
|
|
||||||
|
// global settings
|
||||||
|
tablesInline bool
|
||||||
|
arraysMultiline bool
|
||||||
|
indentSymbol string
|
||||||
|
indentTables bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEncoder returns a new Encoder that writes to w.
|
||||||
|
func NewEncoder(w io.Writer) *Encoder {
|
||||||
|
return &Encoder{
|
||||||
|
w: w,
|
||||||
|
indentSymbol: " ",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTablesInline forces the encoder to emit all tables inline.
|
||||||
|
//
|
||||||
|
// This behavior can be controlled on an individual struct field basis with the
|
||||||
|
// inline tag:
|
||||||
|
//
|
||||||
|
// MyField `inline:"true"`
|
||||||
|
func (enc *Encoder) SetTablesInline(inline bool) *Encoder {
|
||||||
|
enc.tablesInline = inline
|
||||||
|
return enc
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetArraysMultiline forces the encoder to emit all arrays with one element per
|
||||||
|
// line.
|
||||||
|
//
|
||||||
|
// This behavior can be controlled on an individual struct field basis with the multiline tag:
|
||||||
|
//
|
||||||
|
// MyField `multiline:"true"`
|
||||||
|
func (enc *Encoder) SetArraysMultiline(multiline bool) *Encoder {
|
||||||
|
enc.arraysMultiline = multiline
|
||||||
|
return enc
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIndentSymbol defines the string that should be used for indentation. The
|
||||||
|
// provided string is repeated for each indentation level. Defaults to two
|
||||||
|
// spaces.
|
||||||
|
func (enc *Encoder) SetIndentSymbol(s string) *Encoder {
|
||||||
|
enc.indentSymbol = s
|
||||||
|
return enc
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIndentTables forces the encoder to intent tables and array tables.
|
||||||
|
func (enc *Encoder) SetIndentTables(indent bool) *Encoder {
|
||||||
|
enc.indentTables = indent
|
||||||
|
return enc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode writes a TOML representation of v to the stream.
|
||||||
|
//
|
||||||
|
// If v cannot be represented to TOML it returns an error.
|
||||||
|
//
|
||||||
|
// Encoding rules
|
||||||
|
//
|
||||||
|
// A top level slice containing only maps or structs is encoded as [[table
|
||||||
|
// array]].
|
||||||
|
//
|
||||||
|
// All slices not matching rule 1 are encoded as [array]. As a result, any map
|
||||||
|
// or struct they contain is encoded as an {inline table}.
|
||||||
|
//
|
||||||
|
// Nil interfaces and nil pointers are not supported.
|
||||||
|
//
|
||||||
|
// Keys in key-values always have one part.
|
||||||
|
//
|
||||||
|
// Intermediate tables are always printed.
|
||||||
|
//
|
||||||
|
// By default, strings are encoded as literal string, unless they contain either
|
||||||
|
// a newline character or a single quote. In that case they are emitted as quoted
|
||||||
|
// strings.
|
||||||
|
//
|
||||||
|
// When encoding structs, fields are encoded in order of definition, with their
|
||||||
|
// exact name.
|
||||||
|
//
|
||||||
|
// Struct tags
|
||||||
|
//
|
||||||
|
// The following struct tags are available to tweak encoding on a per-field
|
||||||
|
// basis:
|
||||||
|
//
|
||||||
|
// toml:"foo"
|
||||||
|
// Changes the name of the key to use for the field to foo.
|
||||||
|
//
|
||||||
|
// multiline:"true"
|
||||||
|
// When the field contains a string, it will be emitted as a quoted
|
||||||
|
// multi-line TOML string.
|
||||||
|
//
|
||||||
|
// inline:"true"
|
||||||
|
// When the field would normally be encoded as a table, it is instead
|
||||||
|
// encoded as an inline table.
|
||||||
|
func (enc *Encoder) Encode(v interface{}) error {
|
||||||
|
var (
|
||||||
|
b []byte
|
||||||
|
ctx encoderCtx
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx.inline = enc.tablesInline
|
||||||
|
|
||||||
|
if v == nil {
|
||||||
|
return fmt.Errorf("toml: cannot encode a nil interface")
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := enc.encode(b, ctx, reflect.ValueOf(v))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = enc.w.Write(b)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("toml: cannot write: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type valueOptions struct {
|
||||||
|
multiline bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type encoderCtx struct {
|
||||||
|
// Current top-level key.
|
||||||
|
parentKey []string
|
||||||
|
|
||||||
|
// Key that should be used for a KV.
|
||||||
|
key string
|
||||||
|
// Extra flag to account for the empty string
|
||||||
|
hasKey bool
|
||||||
|
|
||||||
|
// Set to true to indicate that the encoder is inside a KV, so that all
|
||||||
|
// tables need to be inlined.
|
||||||
|
insideKv bool
|
||||||
|
|
||||||
|
// Set to true to skip the first table header in an array table.
|
||||||
|
skipTableHeader bool
|
||||||
|
|
||||||
|
// Should the next table be encoded as inline
|
||||||
|
inline bool
|
||||||
|
|
||||||
|
// Indentation level
|
||||||
|
indent int
|
||||||
|
|
||||||
|
// Options coming from struct tags
|
||||||
|
options valueOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *encoderCtx) shiftKey() {
|
||||||
|
if ctx.hasKey {
|
||||||
|
ctx.parentKey = append(ctx.parentKey, ctx.key)
|
||||||
|
ctx.clearKey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *encoderCtx) setKey(k string) {
|
||||||
|
ctx.key = k
|
||||||
|
ctx.hasKey = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *encoderCtx) clearKey() {
|
||||||
|
ctx.key = ""
|
||||||
|
ctx.hasKey = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *encoderCtx) isRoot() bool {
|
||||||
|
return len(ctx.parentKey) == 0 && !ctx.hasKey
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:cyclop,funlen
|
||||||
|
func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
||||||
|
if !v.IsZero() {
|
||||||
|
i, ok := v.Interface().(time.Time)
|
||||||
|
if ok {
|
||||||
|
return i.AppendFormat(b, time.RFC3339), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Type().Implements(textMarshalerType) {
|
||||||
|
if ctx.isRoot() {
|
||||||
|
return nil, fmt.Errorf("toml: type %s implementing the TextMarshaler interface cannot be a root element", v.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
text, err := v.Interface().(encoding.TextMarshaler).MarshalText()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b = enc.encodeString(b, string(text), ctx.options)
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v.Kind() {
|
||||||
|
// containers
|
||||||
|
case reflect.Map:
|
||||||
|
return enc.encodeMap(b, ctx, v)
|
||||||
|
case reflect.Struct:
|
||||||
|
return enc.encodeStruct(b, ctx, v)
|
||||||
|
case reflect.Slice:
|
||||||
|
return enc.encodeSlice(b, ctx, v)
|
||||||
|
case reflect.Interface:
|
||||||
|
if v.IsNil() {
|
||||||
|
return nil, fmt.Errorf("toml: encoding a nil interface is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
return enc.encode(b, ctx, v.Elem())
|
||||||
|
case reflect.Ptr:
|
||||||
|
if v.IsNil() {
|
||||||
|
return enc.encode(b, ctx, reflect.Zero(v.Type().Elem()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return enc.encode(b, ctx, v.Elem())
|
||||||
|
|
||||||
|
// values
|
||||||
|
case reflect.String:
|
||||||
|
b = enc.encodeString(b, v.String(), ctx.options)
|
||||||
|
case reflect.Float32:
|
||||||
|
if math.Trunc(v.Float()) == v.Float() {
|
||||||
|
b = strconv.AppendFloat(b, v.Float(), 'f', 1, 32)
|
||||||
|
} else {
|
||||||
|
b = strconv.AppendFloat(b, v.Float(), 'f', -1, 32)
|
||||||
|
}
|
||||||
|
case reflect.Float64:
|
||||||
|
if math.Trunc(v.Float()) == v.Float() {
|
||||||
|
b = strconv.AppendFloat(b, v.Float(), 'f', 1, 64)
|
||||||
|
} else {
|
||||||
|
b = strconv.AppendFloat(b, v.Float(), 'f', -1, 64)
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
if v.Bool() {
|
||||||
|
b = append(b, "true"...)
|
||||||
|
} else {
|
||||||
|
b = append(b, "false"...)
|
||||||
|
}
|
||||||
|
case reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint:
|
||||||
|
b = strconv.AppendUint(b, v.Uint(), 10)
|
||||||
|
case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int:
|
||||||
|
b = strconv.AppendInt(b, v.Int(), 10)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("toml: cannot encode value of type %s", v.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNil(v reflect.Value) bool {
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Ptr, reflect.Interface, reflect.Map:
|
||||||
|
return v.IsNil()
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v reflect.Value) ([]byte, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if !ctx.hasKey {
|
||||||
|
panic("caller of encodeKv should have set the key in the context")
|
||||||
|
}
|
||||||
|
b = enc.indent(ctx.indent, b)
|
||||||
|
|
||||||
|
b, err = enc.encodeKey(b, ctx.key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b = append(b, " = "...)
|
||||||
|
|
||||||
|
// create a copy of the context because the value of a KV shouldn't
|
||||||
|
// modify the global context.
|
||||||
|
subctx := ctx
|
||||||
|
subctx.insideKv = true
|
||||||
|
subctx.shiftKey()
|
||||||
|
subctx.options = options
|
||||||
|
|
||||||
|
b, err = enc.encode(b, subctx, v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const literalQuote = '\''
|
||||||
|
|
||||||
|
func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) []byte {
|
||||||
|
if needsQuoting(v) {
|
||||||
|
return enc.encodeQuotedString(options.multiline, b, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return enc.encodeLiteralString(b, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func needsQuoting(v string) bool {
|
||||||
|
return strings.ContainsAny(v, "'\b\f\n\r\t")
|
||||||
|
}
|
||||||
|
|
||||||
|
// caller should have checked that the string does not contain new lines or ' .
|
||||||
|
func (enc *Encoder) encodeLiteralString(b []byte, v string) []byte {
|
||||||
|
b = append(b, literalQuote)
|
||||||
|
b = append(b, v...)
|
||||||
|
b = append(b, literalQuote)
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:cyclop
|
||||||
|
func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byte {
|
||||||
|
stringQuote := `"`
|
||||||
|
|
||||||
|
if multiline {
|
||||||
|
stringQuote = `"""`
|
||||||
|
}
|
||||||
|
|
||||||
|
b = append(b, stringQuote...)
|
||||||
|
if multiline {
|
||||||
|
b = append(b, '\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
hextable = "0123456789ABCDEF"
|
||||||
|
// U+0000 to U+0008, U+000A to U+001F, U+007F
|
||||||
|
nul = 0x0
|
||||||
|
bs = 0x8
|
||||||
|
lf = 0xa
|
||||||
|
us = 0x1f
|
||||||
|
del = 0x7f
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, r := range []byte(v) {
|
||||||
|
switch r {
|
||||||
|
case '\\':
|
||||||
|
b = append(b, `\\`...)
|
||||||
|
case '"':
|
||||||
|
b = append(b, `\"`...)
|
||||||
|
case '\b':
|
||||||
|
b = append(b, `\b`...)
|
||||||
|
case '\f':
|
||||||
|
b = append(b, `\f`...)
|
||||||
|
case '\n':
|
||||||
|
if multiline {
|
||||||
|
b = append(b, r)
|
||||||
|
} else {
|
||||||
|
b = append(b, `\n`...)
|
||||||
|
}
|
||||||
|
case '\r':
|
||||||
|
b = append(b, `\r`...)
|
||||||
|
case '\t':
|
||||||
|
b = append(b, `\t`...)
|
||||||
|
default:
|
||||||
|
switch {
|
||||||
|
case r >= nul && r <= bs, r >= lf && r <= us, r == del:
|
||||||
|
b = append(b, `\u00`...)
|
||||||
|
b = append(b, hextable[r>>4])
|
||||||
|
b = append(b, hextable[r&0x0f])
|
||||||
|
default:
|
||||||
|
b = append(b, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b = append(b, stringQuote...)
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// called should have checked that the string is in A-Z / a-z / 0-9 / - / _ .
|
||||||
|
func (enc *Encoder) encodeUnquotedKey(b []byte, v string) []byte {
|
||||||
|
return append(b, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) encodeTableHeader(ctx encoderCtx, b []byte) ([]byte, error) {
|
||||||
|
if len(ctx.parentKey) == 0 {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b = enc.indent(ctx.indent, b)
|
||||||
|
|
||||||
|
b = append(b, '[')
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
b, err = enc.encodeKey(b, ctx.parentKey[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, k := range ctx.parentKey[1:] {
|
||||||
|
b = append(b, '.')
|
||||||
|
|
||||||
|
b, err = enc.encodeKey(b, k)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b = append(b, "]\n"...)
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:cyclop
|
||||||
|
func (enc *Encoder) encodeKey(b []byte, k string) ([]byte, error) {
|
||||||
|
needsQuotation := false
|
||||||
|
cannotUseLiteral := false
|
||||||
|
|
||||||
|
for _, c := range k {
|
||||||
|
if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == '\n' {
|
||||||
|
return nil, fmt.Errorf("toml: new line characters in keys are not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == literalQuote {
|
||||||
|
cannotUseLiteral = true
|
||||||
|
}
|
||||||
|
|
||||||
|
needsQuotation = true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case cannotUseLiteral:
|
||||||
|
return enc.encodeQuotedString(false, b, k), nil
|
||||||
|
case needsQuotation:
|
||||||
|
return enc.encodeLiteralString(b, k), nil
|
||||||
|
default:
|
||||||
|
return enc.encodeUnquotedKey(b, k), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
||||||
|
if v.Type().Key().Kind() != reflect.String {
|
||||||
|
return nil, fmt.Errorf("toml: type %s is not supported as a map key", v.Type().Key().Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
t table
|
||||||
|
emptyValueOptions valueOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
iter := v.MapRange()
|
||||||
|
for iter.Next() {
|
||||||
|
k := iter.Key().String()
|
||||||
|
v := iter.Value()
|
||||||
|
|
||||||
|
if isNil(v) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if willConvertToTableOrArrayTable(ctx, v) {
|
||||||
|
t.pushTable(k, v, emptyValueOptions)
|
||||||
|
} else {
|
||||||
|
t.pushKV(k, v, emptyValueOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortEntriesByKey(t.kvs)
|
||||||
|
sortEntriesByKey(t.tables)
|
||||||
|
|
||||||
|
return enc.encodeTable(b, ctx, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortEntriesByKey(e []entry) {
|
||||||
|
sort.Slice(e, func(i, j int) bool {
|
||||||
|
return e[i].Key < e[j].Key
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type entry struct {
|
||||||
|
Key string
|
||||||
|
Value reflect.Value
|
||||||
|
Options valueOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
type table struct {
|
||||||
|
kvs []entry
|
||||||
|
tables []entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *table) pushKV(k string, v reflect.Value, options valueOptions) {
|
||||||
|
t.kvs = append(t.kvs, entry{Key: k, Value: v, Options: options})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *table) pushTable(k string, v reflect.Value, options valueOptions) {
|
||||||
|
t.tables = append(t.tables, entry{Key: k, Value: v, Options: options})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
||||||
|
var t table
|
||||||
|
|
||||||
|
//nolint:godox
|
||||||
|
// TODO: cache this?
|
||||||
|
typ := v.Type()
|
||||||
|
for i := 0; i < typ.NumField(); i++ {
|
||||||
|
fieldType := typ.Field(i)
|
||||||
|
|
||||||
|
// only consider exported fields
|
||||||
|
if fieldType.PkgPath != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
k, ok := fieldType.Tag.Lookup("toml")
|
||||||
|
if !ok {
|
||||||
|
k = fieldType.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// special field name to skip field
|
||||||
|
if k == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
f := v.Field(i)
|
||||||
|
|
||||||
|
if isNil(f) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
options := valueOptions{
|
||||||
|
multiline: fieldBoolTag(fieldType, "multiline"),
|
||||||
|
}
|
||||||
|
|
||||||
|
inline := fieldBoolTag(fieldType, "inline")
|
||||||
|
|
||||||
|
if inline || !willConvertToTableOrArrayTable(ctx, f) {
|
||||||
|
t.pushKV(k, f, options)
|
||||||
|
} else {
|
||||||
|
t.pushTable(k, f, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return enc.encodeTable(b, ctx, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldBoolTag(field reflect.StructField, tag string) bool {
|
||||||
|
x, ok := field.Tag.Lookup(tag)
|
||||||
|
|
||||||
|
return ok && x == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:cyclop
|
||||||
|
func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ctx.shiftKey()
|
||||||
|
|
||||||
|
if ctx.insideKv || (ctx.inline && !ctx.isRoot()) {
|
||||||
|
return enc.encodeTableInline(b, ctx, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ctx.skipTableHeader {
|
||||||
|
b, err = enc.encodeTableHeader(ctx, b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if enc.indentTables && len(ctx.parentKey) > 0 {
|
||||||
|
ctx.indent++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.skipTableHeader = false
|
||||||
|
|
||||||
|
for _, kv := range t.kvs {
|
||||||
|
ctx.setKey(kv.Key)
|
||||||
|
|
||||||
|
b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b = append(b, '\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range t.tables {
|
||||||
|
ctx.setKey(table.Key)
|
||||||
|
|
||||||
|
ctx.options = table.Options
|
||||||
|
|
||||||
|
b, err = enc.encode(b, ctx, table.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b = append(b, '\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) encodeTableInline(b []byte, ctx encoderCtx, t table) ([]byte, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
b = append(b, '{')
|
||||||
|
|
||||||
|
first := true
|
||||||
|
for _, kv := range t.kvs {
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
} else {
|
||||||
|
b = append(b, `, `...)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.setKey(kv.Key)
|
||||||
|
|
||||||
|
b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(t.tables) > 0 {
|
||||||
|
panic("inline table cannot contain nested tables, online key-values")
|
||||||
|
}
|
||||||
|
|
||||||
|
b = append(b, "}"...)
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func willConvertToTable(ctx encoderCtx, v reflect.Value) bool {
|
||||||
|
if !v.IsValid() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if v.Type() == timeType || v.Type().Implements(textMarshalerType) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
t := v.Type()
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Map, reflect.Struct:
|
||||||
|
return !ctx.inline
|
||||||
|
case reflect.Interface:
|
||||||
|
return willConvertToTable(ctx, v.Elem())
|
||||||
|
case reflect.Ptr:
|
||||||
|
if v.IsNil() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return willConvertToTable(ctx, v.Elem())
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func willConvertToTableOrArrayTable(ctx encoderCtx, v reflect.Value) bool {
|
||||||
|
t := v.Type()
|
||||||
|
|
||||||
|
if t.Kind() == reflect.Interface {
|
||||||
|
return willConvertToTableOrArrayTable(ctx, v.Elem())
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Kind() == reflect.Slice {
|
||||||
|
if v.Len() == 0 {
|
||||||
|
// An empty slice should be a kv = [].
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
t := willConvertToTable(ctx, v.Index(i))
|
||||||
|
|
||||||
|
if !t {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return willConvertToTable(ctx, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) encodeSlice(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
||||||
|
if v.Len() == 0 {
|
||||||
|
b = append(b, "[]"...)
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if willConvertToTableOrArrayTable(ctx, v) {
|
||||||
|
return enc.encodeSliceAsArrayTable(b, ctx, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return enc.encodeSliceAsArray(b, ctx, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// caller should have checked that v is a slice that only contains values that
|
||||||
|
// encode into tables.
|
||||||
|
func (enc *Encoder) encodeSliceAsArrayTable(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
||||||
|
ctx.shiftKey()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
scratch := make([]byte, 0, 64)
|
||||||
|
scratch = append(scratch, "[["...)
|
||||||
|
|
||||||
|
for i, k := range ctx.parentKey {
|
||||||
|
if i > 0 {
|
||||||
|
scratch = append(scratch, '.')
|
||||||
|
}
|
||||||
|
|
||||||
|
scratch, err = enc.encodeKey(scratch, k)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scratch = append(scratch, "]]\n"...)
|
||||||
|
ctx.skipTableHeader = true
|
||||||
|
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
b = append(b, scratch...)
|
||||||
|
|
||||||
|
b, err = enc.encode(b, ctx, v.Index(i))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) encodeSliceAsArray(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) {
|
||||||
|
multiline := ctx.options.multiline || enc.arraysMultiline
|
||||||
|
separator := ", "
|
||||||
|
|
||||||
|
b = append(b, '[')
|
||||||
|
|
||||||
|
subCtx := ctx
|
||||||
|
subCtx.options = valueOptions{}
|
||||||
|
|
||||||
|
if multiline {
|
||||||
|
separator = ",\n"
|
||||||
|
|
||||||
|
b = append(b, '\n')
|
||||||
|
|
||||||
|
subCtx.indent++
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
first := true
|
||||||
|
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
} else {
|
||||||
|
b = append(b, separator...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if multiline {
|
||||||
|
b = enc.indent(subCtx.indent, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err = enc.encode(b, subCtx, v.Index(i))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if multiline {
|
||||||
|
b = append(b, '\n')
|
||||||
|
b = enc.indent(ctx.indent, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
b = append(b, ']')
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (enc *Encoder) indent(level int, b []byte) []byte {
|
||||||
|
for i := 0; i < level; i++ {
|
||||||
|
b = append(b, enc.indentSymbol...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
@@ -0,0 +1,847 @@
|
|||||||
|
package toml_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml/v2"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint:funlen
|
||||||
|
func TestMarshal(t *testing.T) {
|
||||||
|
someInt := 42
|
||||||
|
|
||||||
|
type structInline struct {
|
||||||
|
A interface{} `inline:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
examples := []struct {
|
||||||
|
desc string
|
||||||
|
v interface{}
|
||||||
|
expected string
|
||||||
|
err bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "simple map and string",
|
||||||
|
v: map[string]string{
|
||||||
|
"hello": "world",
|
||||||
|
},
|
||||||
|
expected: "hello = 'world'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "map with new line in key",
|
||||||
|
v: map[string]string{
|
||||||
|
"hel\nlo": "world",
|
||||||
|
},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: `map with " in key`,
|
||||||
|
v: map[string]string{
|
||||||
|
`hel"lo`: "world",
|
||||||
|
},
|
||||||
|
expected: `'hel"lo' = 'world'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "map in map and string",
|
||||||
|
v: map[string]map[string]string{
|
||||||
|
"table": {
|
||||||
|
"hello": "world",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
[table]
|
||||||
|
hello = 'world'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "map in map in map and string",
|
||||||
|
v: map[string]map[string]map[string]string{
|
||||||
|
"this": {
|
||||||
|
"is": {
|
||||||
|
"a": "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
[this]
|
||||||
|
[this.is]
|
||||||
|
a = 'test'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "map in map in map and string with values",
|
||||||
|
v: map[string]interface{}{
|
||||||
|
"this": map[string]interface{}{
|
||||||
|
"is": map[string]string{
|
||||||
|
"a": "test",
|
||||||
|
},
|
||||||
|
"also": "that",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
[this]
|
||||||
|
also = 'that'
|
||||||
|
[this.is]
|
||||||
|
a = 'test'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "simple string array",
|
||||||
|
v: map[string][]string{
|
||||||
|
"array": {"one", "two", "three"},
|
||||||
|
},
|
||||||
|
expected: `array = ['one', 'two', 'three']`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "empty string array",
|
||||||
|
v: map[string][]string{},
|
||||||
|
expected: ``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "map",
|
||||||
|
v: map[string][]string{},
|
||||||
|
expected: ``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "nested string arrays",
|
||||||
|
v: map[string][][]string{
|
||||||
|
"array": {{"one", "two"}, {"three"}},
|
||||||
|
},
|
||||||
|
expected: `array = [['one', 'two'], ['three']]`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "mixed strings and nested string arrays",
|
||||||
|
v: map[string][]interface{}{
|
||||||
|
"array": {"a string", []string{"one", "two"}, "last"},
|
||||||
|
},
|
||||||
|
expected: `array = ['a string', ['one', 'two'], 'last']`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "array of maps",
|
||||||
|
v: map[string][]map[string]string{
|
||||||
|
"top": {
|
||||||
|
{"map1.1": "v1.1"},
|
||||||
|
{"map2.1": "v2.1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
[[top]]
|
||||||
|
'map1.1' = 'v1.1'
|
||||||
|
[[top]]
|
||||||
|
'map2.1' = 'v2.1'
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "map with two keys",
|
||||||
|
v: map[string]string{
|
||||||
|
"key1": "value1",
|
||||||
|
"key2": "value2",
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
key1 = 'value1'
|
||||||
|
key2 = 'value2'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "simple struct",
|
||||||
|
v: struct {
|
||||||
|
A string
|
||||||
|
}{
|
||||||
|
A: "foo",
|
||||||
|
},
|
||||||
|
expected: `A = 'foo'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "one level of structs within structs",
|
||||||
|
v: struct {
|
||||||
|
A interface{}
|
||||||
|
}{
|
||||||
|
A: struct {
|
||||||
|
K1 string
|
||||||
|
K2 string
|
||||||
|
}{
|
||||||
|
K1: "v1",
|
||||||
|
K2: "v2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
[A]
|
||||||
|
K1 = 'v1'
|
||||||
|
K2 = 'v2'
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "structs in array with interfaces",
|
||||||
|
v: map[string]interface{}{
|
||||||
|
"root": map[string]interface{}{
|
||||||
|
"nested": []interface{}{
|
||||||
|
map[string]interface{}{"name": "Bob"},
|
||||||
|
map[string]interface{}{"name": "Alice"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
[root]
|
||||||
|
[[root.nested]]
|
||||||
|
name = 'Bob'
|
||||||
|
[[root.nested]]
|
||||||
|
name = 'Alice'
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "string escapes",
|
||||||
|
v: map[string]interface{}{
|
||||||
|
"a": `'"\`,
|
||||||
|
},
|
||||||
|
expected: `a = "'\"\\"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "string utf8 low",
|
||||||
|
v: map[string]interface{}{
|
||||||
|
"a": "'Ę",
|
||||||
|
},
|
||||||
|
expected: `a = "'Ę"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "string utf8 low 2",
|
||||||
|
v: map[string]interface{}{
|
||||||
|
"a": "'\u10A85",
|
||||||
|
},
|
||||||
|
expected: "a = \"'\u10A85\"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "string utf8 low 2",
|
||||||
|
v: map[string]interface{}{
|
||||||
|
"a": "'\u10A85",
|
||||||
|
},
|
||||||
|
expected: "a = \"'\u10A85\"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "emoji",
|
||||||
|
v: map[string]interface{}{
|
||||||
|
"a": "'😀",
|
||||||
|
},
|
||||||
|
expected: "a = \"'😀\"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "control char",
|
||||||
|
v: map[string]interface{}{
|
||||||
|
"a": "'\u001A",
|
||||||
|
},
|
||||||
|
expected: `a = "'\u001A"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "multi-line string",
|
||||||
|
v: map[string]interface{}{
|
||||||
|
"a": "hello\nworld",
|
||||||
|
},
|
||||||
|
expected: `a = "hello\nworld"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "multi-line forced",
|
||||||
|
v: struct {
|
||||||
|
A string `multiline:"true"`
|
||||||
|
}{
|
||||||
|
A: "hello\nworld",
|
||||||
|
},
|
||||||
|
expected: `A = """
|
||||||
|
hello
|
||||||
|
world"""`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "inline field",
|
||||||
|
v: struct {
|
||||||
|
A map[string]string `inline:"true"`
|
||||||
|
B map[string]string
|
||||||
|
}{
|
||||||
|
A: map[string]string{
|
||||||
|
"isinline": "yes",
|
||||||
|
},
|
||||||
|
B: map[string]string{
|
||||||
|
"isinline": "no",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
A = {isinline = 'yes'}
|
||||||
|
[B]
|
||||||
|
isinline = 'no'
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "mutiline array int",
|
||||||
|
v: struct {
|
||||||
|
A []int `multiline:"true"`
|
||||||
|
B []int
|
||||||
|
}{
|
||||||
|
A: []int{1, 2, 3, 4},
|
||||||
|
B: []int{1, 2, 3, 4},
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
A = [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4
|
||||||
|
]
|
||||||
|
B = [1, 2, 3, 4]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "mutiline array in array",
|
||||||
|
v: struct {
|
||||||
|
A [][]int `multiline:"true"`
|
||||||
|
}{
|
||||||
|
A: [][]int{{1, 2}, {3, 4}},
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
A = [
|
||||||
|
[1, 2],
|
||||||
|
[3, 4]
|
||||||
|
]
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "nil interface not supported at root",
|
||||||
|
v: nil,
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "nil interface not supported in slice",
|
||||||
|
v: map[string]interface{}{
|
||||||
|
"a": []interface{}{"a", nil, 2},
|
||||||
|
},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "nil pointer in slice uses zero value",
|
||||||
|
v: struct {
|
||||||
|
A []*int
|
||||||
|
}{
|
||||||
|
A: []*int{nil},
|
||||||
|
},
|
||||||
|
expected: `A = [0]`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "nil pointer in slice uses zero value",
|
||||||
|
v: struct {
|
||||||
|
A []*int
|
||||||
|
}{
|
||||||
|
A: []*int{nil},
|
||||||
|
},
|
||||||
|
expected: `A = [0]`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "pointer in slice",
|
||||||
|
v: struct {
|
||||||
|
A []*int
|
||||||
|
}{
|
||||||
|
A: []*int{&someInt},
|
||||||
|
},
|
||||||
|
expected: `A = [42]`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "inline table in inline table",
|
||||||
|
v: structInline{
|
||||||
|
A: structInline{
|
||||||
|
A: structInline{
|
||||||
|
A: "hello",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `A = {A = {A = 'hello'}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "empty slice in map",
|
||||||
|
v: map[string][]string{
|
||||||
|
"a": {},
|
||||||
|
},
|
||||||
|
expected: `a = []`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "map in slice",
|
||||||
|
v: map[string][]map[string]string{
|
||||||
|
"a": {{"hello": "world"}},
|
||||||
|
},
|
||||||
|
expected: `
|
||||||
|
[[a]]
|
||||||
|
hello = 'world'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "newline in map in slice",
|
||||||
|
v: map[string][]map[string]string{
|
||||||
|
"a\n": {{"hello": "world"}},
|
||||||
|
},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "newline in map in slice",
|
||||||
|
v: map[string][]map[string]*customTextMarshaler{
|
||||||
|
"a": {{"hello": &customTextMarshaler{1}}},
|
||||||
|
},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "empty slice of empty struct",
|
||||||
|
v: struct {
|
||||||
|
A []struct{}
|
||||||
|
}{
|
||||||
|
A: []struct{}{},
|
||||||
|
},
|
||||||
|
expected: `A = []`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "nil field is ignored",
|
||||||
|
v: struct {
|
||||||
|
A interface{}
|
||||||
|
}{
|
||||||
|
A: nil,
|
||||||
|
},
|
||||||
|
expected: ``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "private fields are ignored",
|
||||||
|
v: struct {
|
||||||
|
Public string
|
||||||
|
private string
|
||||||
|
}{
|
||||||
|
Public: "shown",
|
||||||
|
private: "hidden",
|
||||||
|
},
|
||||||
|
expected: `Public = 'shown'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "fields tagged - are ignored",
|
||||||
|
v: struct {
|
||||||
|
Public string `toml:"-"`
|
||||||
|
private string
|
||||||
|
}{
|
||||||
|
Public: "hidden",
|
||||||
|
},
|
||||||
|
expected: ``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "nil value in map is ignored",
|
||||||
|
v: map[string]interface{}{
|
||||||
|
"A": nil,
|
||||||
|
},
|
||||||
|
expected: ``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "new line in table key",
|
||||||
|
v: map[string]interface{}{
|
||||||
|
"hello\nworld": 42,
|
||||||
|
},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "new line in parent of nested table key",
|
||||||
|
v: map[string]interface{}{
|
||||||
|
"hello\nworld": map[string]interface{}{
|
||||||
|
"inner": 42,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "new line in nested table key",
|
||||||
|
v: map[string]interface{}{
|
||||||
|
"parent": map[string]interface{}{
|
||||||
|
"in\ner": map[string]interface{}{
|
||||||
|
"foo": 42,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "invalid map key",
|
||||||
|
v: map[int]interface{}{},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "unhandled type",
|
||||||
|
v: struct {
|
||||||
|
A chan int
|
||||||
|
}{
|
||||||
|
A: make(chan int),
|
||||||
|
},
|
||||||
|
err: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "numbers",
|
||||||
|
v: struct {
|
||||||
|
A float32
|
||||||
|
B uint64
|
||||||
|
C uint32
|
||||||
|
D uint16
|
||||||
|
E uint8
|
||||||
|
F uint
|
||||||
|
G int64
|
||||||
|
H int32
|
||||||
|
I int16
|
||||||
|
J int8
|
||||||
|
K int
|
||||||
|
}{
|
||||||
|
A: 1.1,
|
||||||
|
B: 42,
|
||||||
|
C: 42,
|
||||||
|
D: 42,
|
||||||
|
E: 42,
|
||||||
|
F: 42,
|
||||||
|
G: 42,
|
||||||
|
H: 42,
|
||||||
|
I: 42,
|
||||||
|
J: 42,
|
||||||
|
K: 42,
|
||||||
|
},
|
||||||
|
|||||||