Compare commits

...
968 Commits
Author SHA1 Message Date
balki 3312c357f3 Support unix sockets and systemd sockets using anyhttp 2026-01-01 17:08:16 -05:00
Dan SosedoffandGitHub fb6091bb83 Update gin to v1.11 (#831) 2025-12-30 11:04:35 -08:00
Alexandru GologanandGitHub 16f88a1336 build with go v1.25 (#814) 2025-12-30 09:01:12 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e0fcd058df Bump github.com/sirupsen/logrus from 1.9.0 to 1.9.1 (#827)
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.9.0 to 1.9.1.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.9.0...v1.9.1)

---
updated-dependencies:
- dependency-name: github.com/sirupsen/logrus
  dependency-version: 1.9.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-27 11:43:53 -08:00
Dan Sosedoff 9e37bd27f1 Update changelog for 0.17.0 2025-11-22 07:53:51 -08:00
Dan Sosedoff 6b0b0244d1 Version bump: 0.17.0 2025-11-22 07:21:34 -08:00
Dan Sosedoff c6ece21133 Remove obsolete reference from docker compose file 2025-11-22 07:19:34 -08:00
Dan Sosedoff 4919407228 Fix ENV reference in Dockerfile 2025-11-22 07:18:35 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1c3c82de18 Bump golang.org/x/crypto from 0.44.0 to 0.45.0 (#823)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.44.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.44.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-22 07:07:41 -08:00
Dan SosedoffandGitHub 06d73805a8 Minor session manager code cleanup and refactor (#822)
* Return a copy of all sessions for session manager Sessions method()
* Use standard mutex Lock method when iterating over stale sessions
* Use defer for consistency
2025-11-16 13:12:28 -08:00
Dan SosedoffandGitHub 74e8482f96 Bump go crypto package to 0.44.0 (#820) 2025-11-15 10:05:15 -08:00
Dan SosedoffandGitHub 79d8afc715 Update dockerfile to use golang 1.24 image (#821) 2025-11-11 22:30:18 -08:00
Dan SosedoffandGitHub 9a5df8487a Use go1.24 in CI and scripts (#818) 2025-11-11 22:18:57 -08:00
Dan SosedoffandGitHub 3fb87562ed Bump golangci-lint version in CI to v1.64.8 (#819) 2025-11-11 12:32:39 -08:00
Dan SosedoffandGitHub 6a52fea44a Add extra test for verifiying ssh connection options (#817)
* Add extra test for verifiying ssh connection options
* Fix failing manager test
* Fix another test
2025-11-11 11:55:42 -08:00
Dan SosedoffandGitHub e3085a44ba Add PostgreSQL 18 support to Github Actions (#816)
* Run against PostgreSQL 18
* Handle constraints for pg18
* Bump golangci-linter to 1.62.2 (#772)
* Bump golangci-linter to 1.62.2
* Address linting warning via change or ignore
2025-11-11 11:30:42 -08:00
Dan SosedoffandGitHub 4ce11c039e Bump golangci-linter to 1.62.2 (#772)
* Bump golangci-linter to 1.62.2
* Address linting warning via change or ignore
2025-11-11 11:26:49 -08:00
Dan SosedoffandGitHub 70f62feec8 Connect backend refactor (#801)
* Move connect backend code to its own package
* Move errors into the connect package
* Add NewBackend func
2025-11-11 10:19:05 -08:00
Dan Sosedoff 266a516076 Update connect backend code to work on latest ruby version 2025-06-15 12:32:30 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2e8800150e Bump rack from 2.2.11 to 2.2.14 in /config/examples/connect_backend_ruby (#792)
Bumps [rack](https://github.com/rack/rack) from 2.2.11 to 2.2.14.
- [Release notes](https://github.com/rack/rack/releases)
- [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rack/rack/compare/v2.2.11...v2.2.14)

---
updated-dependencies:
- dependency-name: rack
  dependency-version: 2.2.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-15 12:26:44 -07:00
Dan SosedoffandGitHub 8413681db0 Fix typo in the list of env vars (#786) 2025-03-30 08:28:18 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ed8a581ff2 Bump rack in /config/examples/connect_backend_ruby (#777)
Bumps [rack](https://github.com/rack/rack) from 2.2.6.2 to 2.2.11.
- [Release notes](https://github.com/rack/rack/releases)
- [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rack/rack/compare/v2.2.6.2...v2.2.11)

---
updated-dependencies:
- dependency-name: rack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-16 10:04:46 -08:00
Dan SosedoffandGitHub 302aa1d85b Shorten git revision printed in the --version output (#770) 2024-12-20 21:30:37 -08:00
Dan SosedoffandGitHub f5f78eb32e Add server settings view (#768) 2024-12-15 14:46:01 -08:00
Oleg SamsonovandGitHub e6df3d19fa add exec time of empty queries (#763)
* add exec time of empty queries
* add { }
* fix space
* use proposed format
2024-11-19 21:55:44 -08:00
Kian-Meng AngandGitHub 08b1ea71e7 Fix typos (#764)
Found via `codespell -S static,data -L selct`
2024-11-17 22:08:29 -08:00
Dan Sosedoff 7fe302ed7c Update changelog formatting 2024-11-02 09:28:51 -07:00
Dan Sosedoff bcd170ab74 Update changelog 2024-11-02 09:26:17 -07:00
Dan Sosedoff e63945ee71 Version bump: 0.16.2 2024-11-02 09:19:33 -07:00
Dan SosedoffandGitHub 487e2820f4 Try running checks against pg 17 (#758)
* Try running checks against pg 17

* Install postgresql-client-17
2024-10-16 20:28:27 -07:00
Dan SosedoffandGitHub a716093236 Update references from master branch to main branch (#750) 2024-09-08 08:17:18 -07:00
Dan Sosedoff f8bfe2de56 Update changelog 2024-09-07 09:49:04 -07:00
Dan Sosedoff 136b2a6942 Version bump: 0.16.1 2024-09-07 09:48:07 -07:00
Dan SosedoffandGitHub c2c69e032c Remove linux/arm/v5 from docker release action (#742) 2024-06-06 19:15:26 -07:00
Dan Sosedoff 3ff70b916f Update changelog 2024-06-04 21:28:17 -07:00
Dan Sosedoff 22eb352180 Version bump: 0.16.0 2024-06-04 21:15:52 -07:00
Dan SosedoffandGitHub 3a32f531a0 Allow database stats downloads (#738)
* Add button to download database stats
* Return xml as well
2024-05-25 10:59:23 -07:00
Dan SosedoffandGitHub 63f1150056 SSH tunnel cleanup and parse fixup (#731) 2024-05-22 20:09:29 -07:00
Dan SosedoffandGitHub 40f582d1ea Add analyze table action (#737) 2024-05-14 20:40:39 -07:00
Dan SosedoffandGitHub cf86131808 Bump postgres version used in docker compose to 15 (#729) 2024-03-27 23:02:46 -07:00
Matt BurdanandGitHub f3d6d9d73e fix: Propagate CGO_ENABLED environment variable to docker build (#724)
* fix: Add ability to statically compile binaries

* propagate env var from github action

* fix comment
2024-03-26 22:44:58 -07:00
Dan SosedoffandGitHub 1b03d78800 Drop linux/arm/v5 from docker build (#728) 2024-03-25 22:44:41 -07:00
Dan SosedoffandGitHub 6489f6a800 Build on 1.22 (#726) 2024-03-22 21:03:07 -07:00
Dan SosedoffandGitHub 36436f3aa7 Switch to go 1.21 (#702)
* Switch to go 1.21

* Update golangci-lint version
2024-03-22 20:36:41 -07:00
Dan SosedoffandGitHub db05989d07 Remove herokuisms (#719) 2024-03-15 22:01:32 -07:00
Dan Sosedoff 3b2bec68ef Update changelog 2024-03-14 22:28:03 -07:00
Dan Sosedoff 9717d3a132 Version bump: 0.15.0 2024-03-14 22:19:59 -07:00
Alexandru GologanandGitHub f4e7643e22 Add support for a bookmarks-only mode (#716)
* Add support for bookmarks-only mode

* Add error for missing bookmarks in bookmarks-only mode

* Error when settings url or connect backend together with bookmarks-only

* Add tests for parsing options
2024-03-14 21:36:53 -07:00
dmahmalatandGitHub 605c483d5b Continue on parseJSON error (#708) 2024-03-11 22:03:37 -07:00
Ashish KulkarniandGitHub 8a8b9f07fb fix missing indexes by quoting schema/table name to ::regclass (#711)
The same approach is used in pkg/statements/sql/table_schema.sql
and a sample reproduction for the bug is:

  CREATE SCHEMA test;
  CREATE TABLE test.data (id INTEGER PRIMARY KEY, name TEXT);
  CREATE UNIQUE INDEX "test.data_uniq" ON test.data (name);

Only the primary key is shown, and not the index created above.
2024-03-11 21:50:48 -07:00
Dan Sosedoff 7ee3c61e38 Version bump: 0.14.3 2024-01-28 20:14:20 -06:00
Dan SosedoffandGitHub 408e23adb3 Allow setting readonly mode in bookmarks (#707) 2024-01-12 21:17:14 -06:00
Dan Sosedoff e560f07de6 License years 2024-01-04 23:34:40 -06:00
Dan SosedoffandGitHub fe5039d17a Allow retrying a connection on startup (#695)
* Allow retrying a connection on startup
* Remove unused vars
* Add an extra comment
* Restructure retry logic a bit
* Update retry logic
* Fix comment
* Update comment
* Change type for RetryCount and RetryDelay to uint
* Extra test cases
* Tweak
2023-11-04 11:12:48 -05:00
Dan SosedoffandGitHub f810c0227b Add UPDATE to list of restricted keywords in read-only mode (#697) 2023-11-02 21:17:46 -05:00
Dan Sosedoff 10c90bcd01 Reformat changelog 2023-10-29 21:29:55 -05:00
Dan Sosedoff f3a6ce4636 Update changelog 2023-10-29 21:18:05 -05:00
Dan Sosedoff 87030c8a17 Version bump: 0.14.2 2023-10-29 21:17:00 -05:00
Dan Sosedoff 614700e69a Bump max ci run to 40 mins 2023-09-26 23:22:59 -05:00
Dan SosedoffandGitHub 5371b4af56 Test with pg16 (#691)
* Test with pg16

* Install pg 16 client
2023-09-26 22:40:57 -05:00
Dan SosedoffandGitHub 22daaad5a2 Fix unclosed database sessions and tunnels (#688) 2023-09-10 11:46:31 -05:00
yanjingtuandGitHub 14d8d80b86 fix when prefix has '/' and prefix is not end with '/' (#684) 2023-08-29 21:09:10 -07:00
Dan SosedoffandGitHub e0c2099e91 Use pg_table_size for table stats query (#685) 2023-07-18 20:15:09 -05:00
Dan Sosedoff a4185415e2 Update changelog 2023-06-17 12:19:21 -05:00
Dan Sosedoff 189ca505bc Version bump: 0.14.1 2023-06-17 12:16:37 -05:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0309d88298 Bump github.com/gin-gonic/gin from 1.7.7 to 1.9.1 (#680)
Bumps [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin) from 1.7.7 to 1.9.1.
- [Release notes](https://github.com/gin-gonic/gin/releases)
- [Changelog](https://github.com/gin-gonic/gin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gin-gonic/gin/compare/v1.7.7...v1.9.1)

---
updated-dependencies:
- dependency-name: github.com/gin-gonic/gin
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-17 12:03:46 -05:00
Dan Sosedoff 60532e852b Add env and envrc into gitignore 2023-06-17 12:01:21 -05:00
Dan SosedoffandGitHub d4cfb059ce Add process start time metric (#675) 2023-05-08 20:46:00 -05:00
Dan SosedoffandGitHub 52f7988ebd Configure pgweb user for docker container (#674)
* Configure pgweb user for docker container
* Set UID to 1000
2023-04-29 14:07:29 -05:00
Dan Sosedoff b2067fbc8d Add clarification comment for binary codec var 2023-04-25 20:02:41 -05:00
Dan Sosedoff f037a4b0d2 Change demo env idle timeout to 30 mins 2023-04-04 15:43:30 -05:00
Dan SosedoffandGitHub 4d32e3b2c5 Switch to go 1.20 (#668)
* Switch to go 1.20
* Fix golang version definition
* Upgrade golangci-lint to v1.51.2
2023-03-31 19:48:14 -05:00
Dan SosedoffandGitHub f94bc9a4dd Fix connection window layout on smaller viewports (#669) 2023-03-31 19:38:38 -05:00
Francesco FrassinelliandGitHub e905e5de53 Better Dockerfile (#645)
* Add support for docker compose build
* Avoid copying more file than needed
* Reduce Docker image size
2023-03-31 18:55:59 -05:00
Dan SosedoffandGitHub 08bbb1537e Implement process uptime metric (#666)
* Implement process uptime metric
* Add clarification comment
* Add another clarification comment
2023-03-30 12:51:49 -05:00
Dan SosedoffandGitHub 941c0acea3 Fix typo in the healthy metric (#657) 2023-02-28 11:57:36 -06:00
TobiasandGitHub bed7ab9564 Use entrypoint instead of cmd in order to facilitate argument passing (#654)
See https://docs.docker.com/engine/reference/builder/#entrypoint and https://docs.docker.com/engine/reference/builder/#cmd.
This allows specifying additional arguments directly after 'docker run ...', e.g. docker run --rm sosedoff/pgweb --listen=8082
2023-02-27 13:14:05 -06:00
Dan Sosedoff d5e72f92b5 Update changelog 2023-02-21 11:49:04 -06:00
Dan Sosedoff 6b3c177909 Version bump: 0.14.0 2023-02-21 11:39:49 -06:00
Dan SosedoffandGitHub c486885159 Fetch local queries on db connect (#650) 2023-02-17 12:26:37 -06:00
Dan Sosedoff fc0be1dcd1 README update 2023-02-15 22:04:37 -06:00
Dan Sosedoff f12cf3725a Move gitconfig step into dockerfile build section 2023-02-15 16:59:39 -06:00
Dan Sosedoff 23fb84db76 Add git config for fly deploy 2023-02-15 16:48:17 -06:00
Ashish KulkarniandGitHub 505ee42867 ensure that objects are sorted by schema and name (#648)
This is a regression from #608
2023-02-15 15:39:17 -06:00
Dan SosedoffandGitHub 41bf189e6b Local queries (#641)
* Read local queries from pgweb home directory
* Refactor local query functionality
* Allow picking local query in the query tab
* WIP
* Disable local query dropdown during execution
* Only allow local queries running in a single session mode
* Add middleware to enforce local query endpoint availability
* Fix query check
* Add query store tests
* Make query store errors portable
* Skip building specific tests on windows
2023-02-02 16:13:14 -06:00
Francesco FrassinelliandGitHub 1c3ab1fd1c Replace DATABASE_URL with PGWEB_DATABASE_URL (#642) 2023-02-02 12:48:42 -06:00
Dan SosedoffandGitHub 5b2d4e4454 Add context menu to display database tables stats (#639)
* Add context menu to display database tables stats

* Move table stats implementation into client
2023-01-23 14:10:26 -06:00
Jens BroekensandGitHub 06212b4c34 Add bookmark options to load username/password from env vars (#638) 2023-01-20 16:59:39 -06:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
79bd9c2f4e Bump rack from 2.2.4 to 2.2.6.2 in /config/examples/connect_backend_ruby (#637)
Bumps [rack](https://github.com/rack/rack) from 2.2.4 to 2.2.6.2.
- [Release notes](https://github.com/rack/rack/releases)
- [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rack/rack/compare/2.2.4...v2.2.6.2)

---
updated-dependencies:
- dependency-name: rack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-18 20:09:55 -06:00
Dan SosedoffandGitHub d7ecb5494d Show error message when API calls fail (#636) 2023-01-18 13:29:04 -06:00
Dan Sosedoff 06be755d56 Update license years 2023-01-17 17:28:45 -06:00
Dan SosedoffandGitHub b2b0094b5e Handle support/permissions errors in info call (#635)
* Handle support/permissions errors in info call
* Fix linting
2023-01-17 16:05:15 -06:00
Dan SosedoffandGitHub 96bd15b3e2 Display cell content via context menu (#634) 2023-01-12 11:55:34 -06:00
Dan SosedoffandGitHub f3353d9007 History page query loading fixup (#632) 2022-12-28 21:56:29 -06:00
Dan Sosedoff a864cbcee8 Update changelog 2022-12-27 19:20:32 -06:00
Dan Sosedoff da671e67cc Version bump: 0.13.1 2022-12-27 19:14:59 -06:00
Dan SosedoffandGitHub 5afecfe52c Fix bookmarks ID check (#631) 2022-12-27 19:03:53 -06:00
Dan Sosedoff fed842a44d Configure fly metrics 2022-12-25 15:06:26 -06:00
Dan Sosedoff 16a897e65d Update changelog 2022-12-25 14:20:38 -06:00
Dan Sosedoff e4530caab6 Version bump: 0.13.0 2022-12-25 14:14:16 -06:00
Dan Sosedoff 6bd8d0f9a7 Enable metrics endpoint on fly 2022-12-25 13:45:10 -06:00
Dan SosedoffandGitHub 239037b4c6 Add query execution stats to api endpoint (#629) 2022-12-25 13:25:18 -06:00
Dan SosedoffandGitHub 12a4bf70c0 Add duration_ms to log entries (#628) 2022-12-25 13:24:22 -06:00
Dan SosedoffandGitHub 1897bef08d Results struct cleanup (#627)
* Add results format test
* Init results slice before formatting data
* Add extra test for nil check in CSV function
2022-12-21 15:02:40 -06:00
Dan Sosedoff aeb9dc24ad Fix mutex unlock call 2022-12-21 14:29:44 -06:00
Dan SosedoffandGitHub bcba666507 Add default connect_timeout option to connection string (#626)
* Add default connect_timeout option to connection string
* Add an extra test
2022-12-20 16:58:54 -06:00
Dan SosedoffandGitHub b31e7f1ea7 Setup basic prom metrics endpoint (#624)
* Setup basic prom metrics endpoint
* Use default prom handler to expose go runtime metrics
2022-12-20 10:13:42 -06:00
Dan Sosedoff 837e25be74 Go mod tidy 2022-12-19 17:51:35 -06:00
Dan SosedoffandGitHub 4e3e255575 Configure timeout and retries when testing connection status (#623) 2022-12-19 16:13:43 -06:00
Dan Sosedoff 362ecb0bc7 Tweak fly config 2022-12-19 13:39:36 -06:00
Dan SosedoffandGitHub 0cd61093b9 Display empty schemas on the sidebar (#621)
* Display empty schemas on the sidebar
* Add schemas fetching test
* Skip pg_temp schemas
* Exclude pg_temp tables from other queries
2022-12-19 13:26:13 -06:00
Dan SosedoffandGitHub 69233cd769 Establish connections using bookmark ID only (#619)
* Establish connections using bookmark ID only
* Refactor specs
* Extra tests
* Fix homedir assertion for bookmarks path
* Fix newline in the warning message
* Check for bookmark file existence before reading
* Connect code restructure
2022-12-19 12:33:13 -06:00
Dan Sosedoff 0b9e7cdb4e Update checks workflow triggers 2022-12-16 15:03:06 -06:00
Dan SosedoffandGitHub afe431c94d Request logging additions (request id, forwarded user) (#618)
* Add request-id logging
* Missing test file
* Add option to log forwarded user information if available via X-Forwarded-Header
* Format
2022-12-16 12:38:57 -06:00
Dan SosedoffandGitHub e0a748812d Add support for .pgpass file (#617)
* Add support for .pgpass file
* Support password lookup in /api/connect endpoint
* Restrore removed code for BuildStringFromOptions
* Restructure connection string test and add extra case for pgpass
* Add test for FormatURL method
2022-12-14 13:37:49 -06:00
Dan Sosedoff 40474d3990 Remove CGO_ENABLED=0 from checks workflow 2022-12-13 15:28:14 -06:00
Dan Sosedoff 41d970e4a7 Remove appveyor build badge 2022-12-13 12:42:24 -06:00
Dan Sosedoff 044367b979 Remove deploy to heroku button 2022-12-13 12:40:04 -06:00
Dan Sosedoff 515f3123dc Update changelog 2022-12-13 12:20:32 -06:00
Dan Sosedoff 81b7c1c6aa Version bump: 0.12.0 2022-12-13 11:59:45 -06:00
Dan SosedoffandGitHub 0dfec506cf Improved Pg test matrix (#616)
* Add postgres 10/11 to test  matrix
* Add docker-compose file fo running multiple postgres versions locally
* Fix client test for pg 10, modify function details to include specific fields
* Try to install latest postgres client
* Add concurrency setting
2022-12-12 18:58:00 -06:00
Dan SosedoffandGitHub 4c40eef99a Perform client version validation before executing pg_dump command (#614)
* Add func to parse out pg_dump version
* Perform client vs server version checking before dump exports
* Fix dump tests
* Add extra test to validate against empty server version
* Fix attachment filenames cleanup function
* Add extra test
* Fix small typos in comments
* Drop third-party package to deal with versions
* Tweak the pg dump incompatibility error message
* Run CI on pull requests
2022-12-12 15:09:12 -06:00
Dan SosedoffandGitHub 7557ac854e Configure client side query timeout (#610)
* Configure client side query timeout
* Update test command on windows
* Make query timeout cli opt an uint
* Fix windows test command
* Check for updates when rendering connection page
* Fix typo
2022-12-08 15:07:40 -06:00
Dan SosedoffandGitHub 0008842a68 Add test for server type and version detection (#612) 2022-12-08 13:33:38 -06:00
Dan Sosedoff 1754274d46 Fix ui flake when switching to activity and connection tabs 2022-12-08 12:43:42 -06:00
Dan SosedoffandGitHub dc2b8f7c5e Switch windows tests from Appveyor to Github Actions (#611)
* Run unit tests on win in gha
* Decomissions appveyor 🫡
2022-12-07 22:17:19 -06:00
Dan SosedoffandGitHub d08dbf34aa Implement global query timeout option (#609)
* Add global query timeout
* Tweak option settings
* Add timeout test
* Move query timeout option close to idle timeout
2022-12-07 18:56:58 -06:00
Dan Sosedoff adf1e4e9ea Change go version in the module 2022-12-07 17:15:46 -06:00
Dan Sosedoff 138fd8af09 Remove stray console.log 2022-12-07 12:04:33 -06:00
Dan SosedoffandGitHub 38051b9465 Add support for user functions (#608)
* Add initial support for functions
* Show functions definitions
* Fix client tests
* Fix schema objects search
* Perform partial matching for functions
* Add function test
* Make sure to close client connections so that database could be dropped in tests
* Fix lint
* Allow to copy the view/functions definitions
* Nits
2022-12-07 11:58:07 -06:00
Dan SosedoffandGitHub bbe9a97d05 Switch go build target to 1.19 (#603)
* Switch go build target to 1.19

* Add make docker-run target for testing new images

* Bump appveyor go version

* Fix appveyor paths
2022-12-06 18:34:08 -06:00
Dan Sosedoff 2c745fa68d Use runtime-provided os/arch info 2022-12-06 18:14:47 -06:00
Dan Sosedoff 88557bfaf5 Fix awk param reference in BUILD_ARCH flag 2022-12-06 18:02:20 -06:00
Dan Sosedoff 829658ae4d Include build arch info into binary 2022-12-06 17:55:27 -06:00
Dan SosedoffandGitHub f48cc5f007 Use go embed to load queries from static files (#607) 2022-12-06 17:41:46 -06:00
Dan SosedoffandGitHub f4b3091666 Merge pull request #605 from sosedoff/log-level
Configure logging level and format
2022-12-06 12:41:56 -06:00
Dan Sosedoff 66fbc730fe Only include form for non-get requests 2022-12-06 12:30:43 -06:00
Dan Sosedoff 2f9d737515 Move request parameters logging into api request logger 2022-12-06 12:24:21 -06:00
Dan Sosedoff dda70352c0 Dont include request path in the log twice 2022-12-06 12:14:55 -06:00
Dan Sosedoff dbd3b26f6c Configure logging level and format 2022-12-06 12:09:21 -06:00
Dan SosedoffandGitHub e6f8153b34 Merge pull request #604 from sosedoff/version-print-cleanup
Add VersionString func to print out full app version
2022-12-06 11:51:42 -06:00
Dan Sosedoff 97e41fbfe5 Add VersionString func to print out full app version 2022-12-06 11:27:47 -06:00
Dan Sosedoff 3e1a93296e Pass ldflags for make install 2022-12-06 11:09:13 -06:00
Dan SosedoffandGitHub e188dd5850 Merge pull request #602 from sosedoff/json-time-marshalling-fix
Fix panic with time marshaling
2022-12-05 21:52:37 -06:00
Dan Sosedoff be81fdab41 Fix panic with time marshalling 2022-12-05 21:47:39 -06:00
Dan SosedoffandGitHub 5228eca9ae Merge pull request #601 from sosedoff/update-delete-with-returning-values
Handle returning values in update/delete queries
2022-12-05 21:12:46 -06:00
Dan Sosedoff e3d69d3cfa Fix lint 2022-12-05 21:01:08 -06:00
Dan Sosedoff 450badfbbb Handle returning values in update/delete queries 2022-12-05 20:56:21 -06:00
Dan SosedoffandGitHub 1cd79005c3 Merge pull request #600 from sosedoff/fly-ci-cd
Setup fly deployment
2022-12-05 18:00:23 -06:00
Dan Sosedoff 443208b217 Change name to demo 2022-12-05 18:00:03 -06:00
Dan Sosedoff 6b0abf1edc Swap the deploy branch 2022-12-05 17:58:26 -06:00
Dan Sosedoff 7ca17fdd2c Fix env var 2022-12-05 17:50:31 -06:00
Dan Sosedoff 098b24c276 Setup fly deployment 2022-12-05 17:49:49 -06:00
Dan SosedoffandGitHub 85b76960c3 Merge pull request #599 from sosedoff/resizable-query-height
Make query input box resizable
2022-12-05 17:14:51 -06:00
Dan Sosedoff 965f1f6eae Set min height of 100pix 2022-12-05 16:59:49 -06:00
Dan Sosedoff 9b086365d0 Minimize resize handlers code 2022-12-05 16:51:33 -06:00
Dan Sosedoff fbc228eb6c Make query input box resizable 2022-12-05 16:35:57 -06:00
Dan SosedoffandGitHub 97497b12f5 Merge pull request #597 from sosedoff/dev-assets-mode
Enable dev assets mode with PGWEB_ASSETS_DEVMODE env var
2022-12-03 20:47:54 -06:00
Dan Sosedoff 695a99dade Enable dev assets mode with PGWEB_ASSETS_DEVMODE env var 2022-12-03 18:27:46 -06:00
Dan SosedoffandGitHub be9a564874 Merge pull request #594 from sosedoff/view-definition-extras
Add ability to view and copy views/materialized views definitions
2022-12-03 17:01:44 -06:00
Dan Sosedoff e6c5075f7c Add ability to display view definition 2022-12-03 16:50:11 -06:00
Dan Sosedoff d3ff23ddfe Add context menu for materialized views 2022-12-03 16:50:11 -06:00
Dan Sosedoff bf6b2f8dda Add ability to copy view definition 2022-12-03 16:50:11 -06:00
Dan SosedoffandGitHub 3cec2c5c66 Merge pull request #596 from sosedoff/fix-flaky-backend-test
Fix flaky backend connection test
2022-12-03 16:49:51 -06:00
Dan SosedoffandGitHub 18f8f35672 Merge pull request #595 from sosedoff/indexes-list-include-sizes
Include index size on the index list view
2022-12-03 16:46:36 -06:00
Dan Sosedoff 1b42caf3a7 Fix flaky backend connection test 2022-12-03 16:42:30 -06:00
Dan Sosedoff 96f82201b5 Fix indexes test 2022-12-03 16:31:07 -06:00
Dan Sosedoff 606227d308 Include index size on the index list view 2022-12-03 16:16:00 -06:00
Dan Sosedoff 96625916e2 Tweak http message logger format 2022-12-03 15:11:45 -06:00
Dan Sosedoff a48f01992f Enable sessions on fly 2022-12-02 14:47:36 -06:00
Dan Sosedoff f1f4edf247 Update link to demo page 2022-12-02 14:45:07 -06:00
Dan SosedoffandGitHub e5480621ee Merge pull request #593 from sosedoff/sessions-manager
Add internal sessions manager
2022-12-02 14:35:43 -06:00
Dan Sosedoff 07d0010750 Make linter happy 2022-12-02 14:22:50 -06:00
Dan Sosedoff 16726e2461 Add idle timeout into session manager 2022-12-02 14:20:20 -06:00
Dan Sosedoff 0a133dc395 Reorder imports 2022-12-02 14:00:01 -06:00
Dan Sosedoff 73dfcc46c3 Verify client is closed 2022-12-02 13:59:22 -06:00
Dan Sosedoff bc54c95140 Tweak test run names 2022-12-02 13:57:56 -06:00
Dan Sosedoff 29a6a0df8c Add session manager tests 2022-12-02 13:56:49 -06:00
Dan Sosedoff 9074d4bfb8 Add internal sessions manager 2022-12-02 13:36:31 -06:00
Dan SosedoffandGitHub d8ee50ece6 Merge pull request #591 from sosedoff/connect-backend-logging
Configure logger for connect backend
2022-12-02 12:27:17 -06:00
Dan SosedoffandGitHub f493d0d04d Merge pull request #592 from sosedoff/default-build-flags
Set LDFLAGS for make build/release commands
2022-12-02 12:21:37 -06:00
Dan Sosedoff e7ac416440 Sanitize connect token in logging 2022-12-02 12:20:58 -06:00
Dan Sosedoff e65ced70ff Set LDFLAGS for make build/release commands 2022-12-02 11:56:18 -06:00
Dan Sosedoff f19f165afc Configure logger for connect backend 2022-12-02 11:45:23 -06:00
Dan Sosedoff f9e376a117 Code cleanup; add make lint task 2022-12-01 16:51:12 -06:00
Dan Sosedoff dafda4a977 Add VersionInfo struct to render in the /info endpoint 2022-12-01 16:41:46 -06:00
Dan Sosedoff 9b8cbb05e3 Include example backend implementation for the connect feature 2022-12-01 16:31:02 -06:00
Dan SosedoffandGitHub 6c3bd96606 Merge pull request #589 from sosedoff/request-logger
Implement gin request logger with param filtering
2022-12-01 13:14:48 -06:00
Dan Sosedoff 72ecd20dd1 Handle logger levels 2022-12-01 13:01:32 -06:00
Dan Sosedoff c996d71378 Implement gin request logger with param filtering 2022-12-01 13:01:32 -06:00
Dan SosedoffandGitHub 1eb86060b5 Merge pull request #590 from sosedoff/appveyor-go-1.18
Test with appveyor on go 1.18
2022-12-01 13:00:37 -06:00
Dan Sosedoff 18f2dcbd7d Test with appveyor on go 1.18 2022-12-01 12:55:41 -06:00
Dan SosedoffandGitHub 9639fa8db7 Merge pull request #588 from sosedoff/bugfix/587
Refactor data export js functionality
2022-12-01 12:11:46 -06:00
Dan Sosedoff c08d8cf617 Automatically add session ID to all generated URLs 2022-12-01 12:07:27 -06:00
Dan Sosedoff f6bd4e3a35 Rename exportURL function 2022-12-01 11:51:47 -06:00
Dan Sosedoff e13c6d0d1a Refactor code responsible for opening export pages in new window 2022-11-30 14:18:31 -06:00
Dan Sosedoff 2a4434a04a Generate a new url based on the old one using URL 2022-11-30 14:03:11 -06:00
Dan Sosedoff dd1fb90355 Refactor bookmarks ssl params 2022-11-23 16:21:30 -06:00
Dan SosedoffandGitHub 84bf1f091b Merge pull request #586 from sosedoff/cli-usage-env-vars
Print available environment variables in help message
2022-11-22 16:21:29 -06:00
Dan Sosedoff c103eed463 Comment fixes 2022-11-22 16:17:28 -06:00
Dan Sosedoff e1aa717b22 Print available environment variables in help message 2022-11-22 16:09:01 -06:00
Dan SosedoffandGitHub ee6898f243 Merge pull request #585 from sosedoff/cli-env-vars-support-with-prefix
Add support for PGWEB_ prefix environment variables
2022-11-22 15:25:07 -06:00
Dan Sosedoff a2a5415ace Print deprecation notice when using unprefixed env var 2022-11-22 15:20:49 -06:00
Dan Sosedoff b1023cdcfd Add support for PGWEB_ prefixed env vars 2022-11-22 14:51:13 -06:00
Dan Sosedoff fa0efb1597 Make getCurrentUser method private 2022-11-22 14:51:13 -06:00
Dan Sosedoff 0a7de05892 Update github.com/jessevdk/go-flags package 2022-11-22 14:51:13 -06:00
Dan SosedoffandGitHub e5281751c1 Merge pull request #584 from sosedoff/github-container-registry
Configure github container registry access for new releases
2022-11-15 17:54:11 -06:00
Dan Sosedoff c2fbfd2e6c Configure github container registry access for new releases 2022-11-14 17:13:41 -06:00
Dan Sosedoff c3ce8cfc3b Only run docker build workflow on master branch 2022-11-14 16:36:33 -06:00
Dan SosedoffandGitHub 3c6d98fa11 Merge pull request #582 from sosedoff/lint
Configure linter runs
2022-11-14 16:35:26 -06:00
Dan Sosedoff a5143ad9d3 Use checkout/setup v3 actions 2022-11-14 16:14:50 -06:00
Dan Sosedoff 9bfec11b48 Fix linting errors 2022-11-14 16:10:50 -06:00
Dan Sosedoff 580c1093d1 Add lint into github workflow 2022-11-14 15:49:30 -06:00
Dan Sosedoff c706523bd0 Lint: replace ioutil with os package 2022-11-14 15:47:36 -06:00
Dan Sosedoff 685c222b9d Lint: replace os.Kill trap with syscall.SIGTERM 2022-11-14 15:43:34 -06:00
Dan Sosedoff added3bb42 Add fly config for demo app 2022-11-14 11:28:32 -06:00
Dan SosedoffandGitHub 6fdf9261da Install latest postgres client (#577)
* Install latest postgres client

* Build on debian bullseye
2022-10-25 07:55:28 -04:00
Dan Sosedoff 13091c2a7c Add make install target 2022-09-18 10:14:51 -05:00
Joubert RedRatandGitHub 1f65fec9ea Add netcat install in dockerfile to provide a way to healthcheck in docker compose (#572) 2022-08-19 09:22:16 -05:00
Dan SosedoffandGitHub 9a75243732 Replace gox with a bash build script (#571) 2022-07-26 20:17:40 -05:00
Dan Sosedoff 2d6ec5dde7 Set build and release actions timeout to 30 mins 2022-07-23 18:46:07 -05:00
Dan Sosedoff fae23dc57c Update release action to use qemu and buildx v2 action 2022-07-23 18:26:19 -05:00
Dan Sosedoff 4607c1bb99 Update changelog 2022-07-05 21:13:08 -05:00
Dan Sosedoff 781469c4cf Version bump: 0.11.12 2022-07-05 21:11:03 -05:00
Dan SosedoffandGitHub ae3b74ad2a Docker build refactor (#568) 2022-06-27 22:31:57 -05:00
Dan SosedoffandGitHub 193073727a Use go 1.18 in github actions (#560) 2022-05-20 20:41:01 -05:00
Dan Sosedoff 9aa3a5a84e Do not run double checks in github actions 2022-05-20 20:39:51 -05:00
Boban AcimovicandGitHub 7b8b9f5c80 update go deps and alpine (#558) 2022-05-20 20:36:11 -05:00
Dan Sosedoff 28229c4fc7 Update changelog and docker image version 2022-03-29 23:35:22 -05:00
Dan Sosedoff db2a7a8aa5 Version bump: 0.11.11 2022-03-29 23:32:36 -05:00
Dan SosedoffandGitHub 89f84367ad Change binary codec back to none (#555) 2022-03-26 18:11:33 -05:00
Dan SosedoffandGitHub 4c096e8774 Auto-detect the query from the query source based on user selection (#547)
* Auto-detect the query from the query source based on user selection

* Misc comment
2022-02-10 20:14:42 -06:00
5803295174 Added binary codec base58 as well as improving the help for --binary-codec flag (#548)
* Added binary codec `base58` as well as improving the help for `--binary-codec` flag
* Added tests for base58
* Fixed tests

Co-authored-by: Matthieu Vachon <matt@streamingfast.io>
2022-01-27 22:10:01 -06:00
Dan Sosedoff ccf901d5c7 Bring back the pull_request github actions trigger 2022-01-24 18:32:15 -06:00
Dan Sosedoff 374dc600e0 Update changelog and docker target 2022-01-20 21:37:05 -06:00
Dan Sosedoff 8171907ec2 Version bump: 0.11.10 2022-01-20 21:28:05 -06:00
Dan Sosedoff 98f328a3fa Appveyor lint 2022-01-08 14:52:21 -06:00
Dan Sosedoff 3b4d0777a1 Bump license years 2022-01-08 14:52:05 -06:00
Dan SosedoffandGitHub e2f5e06c07 Add connect backend tests (#546) 2022-01-08 14:45:21 -06:00
Dan SosedoffandGitHub 0794c642e4 Use HTTP 302 status code for successful backend redirect (#544) 2022-01-08 12:25:24 -06:00
Dan Sosedoff 4c496feff2 Run github actions on pushes only 2022-01-04 17:17:30 -06:00
Dan SosedoffandGitHub 7718c249d0 Use go1.17 in go mod (#543) 2022-01-03 21:11:02 -06:00
Tim IngsandGitHub 17e95601ec Explain analyze dropdown button (#532)
* Adds bootstrap dropdown plugin
* Float dropdown toggle left
* Extract query running message toggles
* Adds analyze function and api call
* Adds analyze api
2022-01-03 19:05:46 -06:00
Dan SosedoffandGitHub af79994595 DRY ldflags; include build time into version string (#541) 2021-12-30 15:33:43 -06:00
Dan SosedoffandGitHub 706caa44bf Serialize binary bytea cols into hex/base64 (#537)
- Adds binary serialization into hex/base64
- Default codec is base64
- Codec can be changed via `--binary-codec` CLI option
2021-12-29 11:03:50 -06:00
Dan Sosedoff 1323012cff Replace travis link in readme 2021-12-25 11:45:10 -06:00
Dan SosedoffandGitHub ccfa3497f3 GitHub actions migration (#540) 2021-12-25 11:31:41 -06:00
Dan SosedoffandGitHub b1b50045a5 Merge pull request #536 from maoueh/feature/remove-alert-on-column-copy
Removes (annoying) `alert` on column copy value
2021-12-08 09:33:27 -06:00
Matthieu Vachon cc1666b16a Removes (annoying) alert on column copy value
All other copy to clipboard action are `alert` free like copying a table name to the clipboard. I think it should be the same for copying a column value.

If you think some feedback is necessary, maybe we should see a small label showing up than closing shortly after. As a comparison, it appears GitHub, in the commit list view, does not shot any feedback anymore (is it a bug maybe? because remote url copy still shows a feedback, anyway :)).

P.S. My editor automatically removed some trailing white spaces, if you prefer, I can revert those.
2021-12-07 23:06:28 -05:00
Dan Sosedoff be6ef48d40 Update changelog 2021-10-08 21:55:10 -05:00
Dan Sosedoff 7ec1e01edd Version bump: 0.11.9 2021-10-08 21:30:45 -05:00
Dan SosedoffandGitHub d44d1e77e8 Merge pull request #520 from tuaris/patch-1
FreeBSD rc.d startup script
2021-09-23 21:42:46 -04:00
Dan SosedoffandGitHub 206f6dd909 Merge pull request #527 from sosedoff/gin-upgrade
Upgrade gin to latest ; go mod tidy
2021-09-23 21:42:21 -04:00
Dan Sosedoff c109f25fae Upgrade gin to latest ; go mod tidy 2021-09-23 20:29:52 -05:00
Dan SosedoffandGitHub eab61f85b9 Merge pull request #522 from chenrui333/patch-2
chore: update image to use `alpine:3.14`
2021-08-17 20:51:25 -05:00
Dan SosedoffandGitHub 0252540c4b Merge pull request #525 from sosedoff/go-embed-path-prefix-fix
Fix broken path prefix
2021-08-17 20:47:18 -05:00
Dan Sosedoff 46935a7a12 Fix broken path prefix 2021-08-10 12:12:05 -05:00
Dan SosedoffandGitHub 360d7d87c5 Merge pull request #521 from chenrui333/patch-1
fix: should be build time rather than runtime
2021-07-22 08:25:49 -05:00
ruiandGitHub 01d4e9b9c6 chore: update image to use alpine:3.14 2021-07-09 15:54:08 -04:00
ruiandGitHub 218800395d fix: should be build time rather than runtime 2021-07-09 15:52:09 -04:00
Daniel MoranteandGitHub 46d84f4b2c FreeBSD rc.d startup script
Startup script for FreeBSD
2021-07-09 02:24:17 -04:00
Dan Sosedoff 3455a89fd5 Update version in dockerfile 2021-07-07 19:43:07 -05:00
Dan Sosedoff 49b6db19eb Update changelog 2021-07-07 19:19:18 -05:00
Dan Sosedoff 4de356cb1d Version bump: 0.11.8 2021-07-07 19:11:42 -05:00
Dan SosedoffandGitHub 9d1a3fd533 Merge pull request #516 from sosedoff/heroku-go-mod
Specify buildpack version for Heroku
2021-05-10 09:18:27 -05:00
Dan Sosedoff 8284c41d6e Specify buildpack version for Heroku 2021-05-10 09:00:09 -05:00
Dan SosedoffandGitHub cef4ab7eaa Merge pull request #513 from sosedoff/apple-silicon-binary-build
Add darwin/arm64 binary build target
2021-04-13 13:31:44 -05:00
Dan Sosedoff b5abd7555f Add darwin/arm64 binary build target 2021-04-12 09:59:15 -05:00
Dan Sosedoff ceece29737 Remove extra whitespace 2021-04-12 09:57:18 -05:00
Dan SosedoffandGitHub 4adba368c4 Merge pull request #510 from MobileTeleSystems/go-1-16-embed
go 1.16 features
2021-03-26 21:54:30 -05:00
Nikita Kazarian 8e2c826ba3 go 1.16 features
- use go embed instead of go-binary and remove all about old assets (go 1.16 feature)
- pin gox version (go 1.16 feature)
- update ci to go 1.16
2021-03-05 03:47:01 +03:00
Dan SosedoffandGitHub 68e6f2d117 Merge pull request #509 from MobileTeleSystems/move-to-go-modules
move project to go modules
2021-03-04 11:46:27 -06:00
Nikita Kazarian 4a3145bfbd add GO111MODULE=on to Makefile for go 1.12 support 2021-03-04 00:31:10 +03:00
Nikita Kazarian 21e8493838 move project to go modules
- remove vendor
- pin cockroach version in tests
- remove golang.org/x/tools/cmd/cover (not maintained and moved to standart library)
2021-03-04 00:15:23 +03:00
Dan Sosedoff a3f0de7916 Remove artifacts build workflow 2021-02-28 13:10:03 -06:00
Dan SosedoffandGitHub 64c5ca5cb1 Merge pull request #497 from camccar/Arm64Build
Add arm64 v7 option
2020-12-07 20:33:22 -06:00
caleb mccarthy 70069e4e23 Add arm64 v7 option 2020-12-05 15:26:57 -06:00
Dan Sosedoff 4066de695d Update changelog, docker version 2020-10-18 10:36:32 -05:00
Dan Sosedoff 31f6261c1c Version bump: 0.11.7 2020-10-18 10:29:42 -05:00
Dan SosedoffandGitHub e864b5f275 Merge pull request #494 from sosedoff/dump-test-error-adjust
Adjust error message matching for dump spec
2020-10-18 10:27:34 -05:00
Dan Sosedoff 86b1ea887c Adjust error message matching for dump spec 2020-10-18 10:18:03 -05:00
Dan Sosedoff 93d51c1feb Readme update 2020-10-18 10:14:04 -05:00
Dan SosedoffandGitHub ce6a777770 Merge pull request #493 from sosedoff/auth-error-regex-improvement
Use regular expression for checking some of the connection errors
2020-10-18 09:53:55 -05:00
Dan SosedoffandGitHub 440ae19c96 Merge pull request #492 from sosedoff/database-search-autocomplete-off
Disable autocomplete on database search field
2020-10-18 09:41:12 -05:00
Dan SosedoffandGitHub 7f66e3e523 Merge pull request #491 from sosedoff/ci-go-1.15
Run specs on go1.15
2020-10-18 09:35:54 -05:00
Dan Sosedoff f61c1c45d5 Use regular expression for checking some of the connectio errors 2020-10-18 09:32:34 -05:00
Dan Sosedoff 707fea6d13 Disable autocomplete on database search field 2020-10-18 09:22:53 -05:00
Dan Sosedoff ffd46c256c Run specs on go1.15 2020-10-18 09:21:29 -05:00
Dan SosedoffandGitHub e1fc9ddcef Merge pull request #489 from allisson/strip-binary
feat(release): strip binary to reduce size
2020-10-18 09:16:51 -05:00
Allisson Azevedo d3da40e0ad feat(release): strip binary to reduce size 2020-10-10 12:05:32 -03:00
Dan SosedoffandGitHub 3d14d42356 Merge pull request #479 from simhnna/patch-1
Do not show other databases if session is locked
2020-07-29 21:20:09 -05:00
Simon HannaandGitHub 27a83bdf9e Do not show other databases if session is locked
Fixes: #470
2020-07-15 18:11:05 +02:00
Dan SosedoffandGitHub a5f9df34fb Merge pull request #474 from jc00ke/master
Add note to README about connecting over Unix domain sockets
2020-06-09 07:40:53 -05:00
Jesse Cooke 92dbbbd5e8 Add note to README about connecting over Unix domain sockets 2020-06-07 16:59:06 -07:00
Dan SosedoffandGitHub 111b1720b4 Merge pull request #463 from sosedoff/skip-local-auth-fail
Do not terminate if local authentication failed on start
2020-04-27 10:15:25 -05:00
Dan Sosedoff 02ddf85af2 Do not terminate if local authentication failed on start 2020-04-23 15:54:12 -05:00
Dan SosedoffandGitHub 82b2918963 Merge pull request #459 from sosedoff/go1.14
Run CI on 1.14
2020-03-28 09:55:08 -05:00
Dan Sosedoff 368b60b7e6 Run CI on 1.14 2020-03-28 09:32:30 -05:00
Dan Sosedoff d9586c5200 Fix postgres runs in test-all script 2020-03-15 20:54:17 -05:00
Dan SosedoffandGitHub 136206fcff Merge pull request #457 from sosedoff/query-results-context-menu
Show results row context menu on custom query results
2020-03-15 20:22:09 -05:00
Dan Sosedoff 678a6fd7cb Show results row context menu on custom query results 2020-03-15 19:50:19 -05:00
Dan Sosedoff 16e3cc3998 Update docker version, changelog 2020-02-19 20:30:36 -06:00
Dan Sosedoff 3e4e9c30c9 Release 0.11.6 2020-02-19 20:24:33 -06:00
Dan Sosedoff 9aa52aa042 Remove contact section 2020-02-12 13:31:43 -06:00
Dan Sosedoff fb83d9635b License years update 2020-02-12 13:31:13 -06:00
Dan SosedoffandGitHub e112e21891 Merge pull request #455 from sosedoff/remote-double-click-on-cell
Remove double click action on cell
2020-02-12 13:28:37 -06:00
Dan Sosedoff 6025b7afb2 Remove double click action on cell
This feature was a source of confusion for new pgweb users for quite
some time now. Most people thought by double-clicking the cell it
would allow them to edit the value since the column is replaced
with textarea. The textarea was there to copy the cell's value, not
for editing. Feature is no longer required due to availability of a
cell context menu, which provides a link to copy the value.
2020-02-12 13:14:58 -06:00
Dan Sosedoff dc390b552d Build binaries for github.ref 2020-02-11 22:49:30 -06:00
Dan SosedoffandGitHub 9fabb602bf Testing github action workflow (#454)
Build and upload linux/mac binaries to S3 (unofficial releases)
2020-02-11 22:37:24 -06:00
Dan SosedoffandGitHub 12405d5106 Merge pull request #452 from sosedoff/ssl-root-cert
Extended SSL options
2020-02-11 11:11:07 -06:00
Dan Sosedoff e2217e5f71 Add tests for ssl params 2020-02-11 10:58:40 -06:00
Dan Sosedoff ce4aea8da1 Add ssl cert/key options 2020-02-05 22:09:13 -06:00
Dan Sosedoff 9c7f566d4d Add option to support sslrootcert parameter 2020-02-05 22:01:25 -06:00
Dan Sosedoff cc35a754e0 Add dockerignore 2019-12-16 17:26:23 -06:00
Dan Sosedoff c9a6065867 Update changelog, bump docker version 2019-12-16 17:23:34 -06:00
Dan Sosedoff 9d0a0e1db1 Release 0.11.5 2019-12-16 17:15:55 -06:00
Dan SosedoffandGitHub 61fc975ac9 Merge pull request #450 from sosedoff/include-go-version-into-info
Include go version into info response
2019-12-04 20:40:02 -06:00
Dan Sosedoff 43a2945208 Include go version into info response 2019-12-04 20:29:23 -06:00
Dan SosedoffandGitHub 0d8502f799 Merge pull request #449 from sosedoff/object-autocompletion
Autocomplete on all database objects
2019-12-04 19:51:40 -06:00
Dan Sosedoff 5d2ec70476 Autocomplete on all database objects 2019-12-04 19:36:09 -06:00
Dan SosedoffandGitHub e54030dee9 Merge pull request #448 from jim68000/master
Fix for visual glitch when schema name wider than 250px
2019-12-04 12:39:42 -06:00
Jim Smith e5c53d598c Fix for visual glitch when schema name wider than 250px 2019-12-04 18:28:14 +00:00
Dan SosedoffandGitHub 07a9b00710 Merge pull request #447 from sosedoff/go-runtime-in-version
Include go version into builds
2019-11-29 14:13:21 -06:00
Dan Sosedoff 8b1f234b91 Include go version into builds 2019-11-29 13:59:50 -06:00
Dan Sosedoff a21ff40588 Add .idea to gitignore 2019-11-16 11:52:15 -06:00
Dan Sosedoff d833df8b0d Update heroku go version to 1.13 2019-11-02 13:45:56 -05:00
Dan Sosedoff bc95e8dced Add extra styling for autocomplete 2019-11-02 13:37:52 -05:00
Dan SosedoffandGitHub 9afc152f25 Merge pull request #443 from oleggator/autocompletion
Add SQL keyword autocompletion
2019-11-02 13:36:49 -05:00
Dan SosedoffandGitHub d9ea14e37f Merge pull request #445 from sosedoff/ssh-key-handling
SSH key handling update
2019-11-02 13:21:49 -05:00
Dan Sosedoff 8428d268b1 Lint 2019-11-02 13:00:23 -05:00
Dan Sosedoff 7475f398b1 Add support for encrypted ssh keys 2019-11-02 12:44:04 -05:00
Dan Sosedoff 9e64bb5eec Update ssh dependency 2019-11-02 11:36:42 -05:00
Dan Sosedoff 994ceca1d1 Return error when ssh key does not exist 2019-11-02 11:36:31 -05:00
Dan Sosedoff f2ab51a314 Update changelog, bump docker image version 2019-10-05 12:54:19 -05:00
Dan Sosedoff 059764d015 Release 0.11.4 2019-10-05 12:48:21 -05:00
Oleg Utkin abf6acf82e add sql keyword autocompletion 2019-10-03 01:20:18 +03:00
Oleg Utkin 50a9a87a6a upgrade ace to 1.4.6 2019-10-03 01:17:29 +03:00
Dan SosedoffandGitHub 7a6450091a Code formatting and cleanup (#442)
* Formatting, extract error messages
* More refactor
* Move errors to a separate file
* Add missing file
* Misc
2019-09-29 12:16:42 -05:00
Dan SosedoffandGitHub c4db1973c4 Merge pull request #441 from sosedoff/drop-pg-dump-params
Drop unsupported pg_dump options from connection string
2019-09-25 20:39:55 -05:00
Dan Sosedoff b6f01da232 Drop unsupported pg_dump options from connection string 2019-09-25 20:29:21 -05:00
Dan Sosedoff d5e4a10137 Fix matrix 2019-09-21 12:49:30 -05:00
Dan Sosedoff 37c861cc81 Update travis matrix 2019-09-21 12:44:39 -05:00
Dan SosedoffandGitHub 1e4a09d8b5 Merge pull request #438 from sd65/fix-sql-export
Fix SQL export filename
2019-07-31 20:45:17 -05:00
Dan SosedoffandGitHub 27ace7cafd Merge pull request #439 from sd65/upgrade-alpine-to-3.10
Upgrade alpine to 3.10 - fix SQL export on up-to-date Postgresql
2019-07-31 10:21:58 -05:00
Sylvain DOIGNON 0bdf8b3498 display image size in docker-release task 2019-07-31 16:43:17 +02:00
Sylvain DOIGNON 0fc6ab8636 Upgrade alpine to 3.10 2019-07-31 15:32:51 +02:00
Sylvain DOIGNON 5bb339e1a2 fix SQL export filename 2019-07-31 14:43:55 +02:00
Dan Sosedoff 1334e48fc7 Update dockerfile 2019-07-24 17:28:15 -05:00
Dan Sosedoff 25af34aa6a Changelog update 2019-07-24 17:13:49 -05:00
Dan Sosedoff c830a55a64 Release 0.11.3 2019-07-24 17:07:27 -05:00
Dan SosedoffandGitHub 7365dbdb11 Merge pull request #435 from sosedoff/update-postgres-dep
Update lib/pg to 1.1.1
2019-07-05 17:42:07 -05:00
Dan Sosedoff f31143ba3a Set odd background color for table rows 2019-07-05 17:28:55 -05:00
Dan Sosedoff 5a50155cd3 Update lib/pg to 1.1.1 2019-07-05 16:53:01 -05:00
Dan Sosedoff 6843ca966b New changelog entries [ci-skip] 2019-07-05 16:31:30 -05:00
Dan SosedoffandGitHub 06a9d8015c Merge pull request #434 from sosedoff/restructure-sidebar
Sidebar restructure
2019-07-05 16:29:32 -05:00
Dan Sosedoff 5c3b21d711 Expland lists of objects on search 2019-07-05 16:20:46 -05:00
Dan Sosedoff 416cae3101 Add database object filter to the sidebar 2019-07-05 16:18:29 -05:00
Dan Sosedoff 819b13fe55 Restructure sidebar 2019-07-05 15:19:05 -05:00
Dan SosedoffandGitHub 15c21b6379 Merge pull request #421 from sosedoff/restrict-keywords
Keyword restriction in read-only mode
2019-02-25 19:18:52 -06:00
Dan SosedoffandGitHub 2b6d955764 Merge pull request #423 from sosedoff/homebrew-update-formula-script
Add script to bump Homebrew formula version
2019-02-25 18:14:23 -06:00
Dan Sosedoff 60337027dc Add script to bump Homebrew formula version 2019-02-25 12:31:46 -06:00
Dan Sosedoff 2a0fd54855 Remove comments from the query before checking for restricted keywords 2019-02-25 11:43:04 -06:00
Dan Sosedoff b83f52707a Remove LOCK/SET from restricted keywords 2019-02-20 20:29:28 -06:00
Dan Sosedoff 5bb6378e94 Switch back readonly mode after test finish 2019-02-20 18:22:44 -06:00
Dan Sosedoff 40eb74529e Reject queries that contain restricted keywords in read-only mode 2019-02-20 18:20:27 -06:00
Dan Sosedoff 9f29b10098 Update changelog/dockerfile 2019-02-15 14:16:34 -06:00
Dan Sosedoff d3aaf8f5db Release 0.11.2 2019-02-15 14:09:49 -06:00
Dan SosedoffandGitHub 26cd70cbaa Merge pull request #414 from sosedoff/fix-estimation-for-camelcase-schemas
Fix estimation for camelcase schemas
2019-02-08 14:25:19 -06:00
Dan Sosedoff 65aae5ec95 Add extra test cases for camelCase table names 2019-02-08 14:02:36 -06:00
Dan Sosedoff 5af4332593 Fix SQL query for camelCase tables row estimation 2019-02-08 13:27:40 -06:00
Dan Sosedoff 12717f15cc Changelog update 2019-01-28 17:18:55 -06:00
Dan Sosedoff dc1009cdd8 Release 0.11.1 2019-01-28 17:11:04 -06:00
Dan SosedoffandGitHub 52bda5f823 Merge pull request #409 from sosedoff/switch-bindata-dep
Switch to github.com/go-bindata/go-bindata fork
2019-01-28 16:51:43 -06:00
Dan Sosedoff 44b6932202 Regenerate bindata 2019-01-28 16:45:41 -06:00
Dan Sosedoff 0556d8b69d Switch to github.com/go-bindata/go-bindata fork 2019-01-28 16:32:43 -06:00
Dan SosedoffandGitHub 01516e030b Merge pull request #408 from sosedoff/fix-auto-user-detection
Fix error when automatic user detection fails
2019-01-28 14:30:57 -06:00
Dan Sosedoff c32d68809b Add extra note 2019-01-28 14:22:05 -06:00
Dan Sosedoff d48172986e Fix error when automatic user detection fails 2019-01-28 14:03:45 -06:00
Dan Sosedoff e6f6dbbddd Update license years 2019-01-28 12:08:25 -06:00
Dan Sosedoff 7995a588bd Remove apt cache when building docker container 2019-01-28 12:02:30 -06:00
Dan SosedoffandGitHub baa587166c Merge pull request #405 from Nhat002/fix_encode_query_to_base64_in_export
Add custome base64 encode to handle special characters on query to export result
2019-01-28 11:47:36 -06:00
Dan SosedoffandGitHub 8dab66567d Merge pull request #406 from sosedoff/fix-startup-error-with-db-url
Fix startup error when DATABASE_URL is set
2019-01-25 11:08:02 -06:00
Dan Sosedoff a7959ad0fa Fix startup error when DATABASE_URL is set 2019-01-25 10:29:24 -06:00
Nhat Tran 25304f8229 Fix: missing base64.js in index.html and remove blank space 2019-01-25 12:04:21 +08:00
Nhat Tran bc0602c523 Add custome base64 encode to handle special character 2019-01-24 15:35:16 +08:00
Dan Sosedoff ae60604ef1 Fix typo 2019-01-16 22:54:23 -06:00
Dan Sosedoff 4bdcfbd5b1 Update changelog/dockerfile 2018-12-24 13:08:20 -06:00
Dan Sosedoff 53c02d8914 Release 0.11.0 2018-12-24 12:57:39 -06:00
Dan SosedoffandGitHub 4d74052ad8 Merge pull request #401 from sosedoff/db-object-counter-style
Tweak sidebar database object counters styles
2018-12-19 19:41:45 -06:00
Dan Sosedoff 435a67cb5d Tweak sidebar database object counters styles 2018-12-19 12:40:28 -06:00
Dan SosedoffandGitHub aadee6ac8a Merge pull request #399 from sosedoff/fix-start-with-default-db
Do not exit with error if local server is not running
2018-12-18 20:23:09 -06:00
Dan Sosedoff 9c07ac356b Do not exit with error if local server is not running 2018-12-18 18:11:56 -06:00
Dan SosedoffandGitHub 9a3c79d93d Merge pull request #398 from sosedoff/ssh-host-callback-fix
SSH connection fix
2018-12-14 20:18:41 -06:00
Dan Sosedoff 2bd9f39da5 Fix SSH error: ssh: must specify HostKeyCallback 2018-12-14 19:56:33 -06:00
Dan Sosedoff c4e63dcc85 Regenerate bindata 2018-12-13 22:36:38 -06:00
Dan Sosedoff 794e5a22c1 Rename command.VERSION to command.Version 2018-12-13 22:35:43 -06:00
Dan Sosedoff eeb5245cb9 Typo fix 2018-12-13 17:56:57 -06:00
Dan SosedoffandGitHub e8328b25bc Merge pull request #397 from sosedoff/restrict-activity
Scope activity list to current database
2018-12-13 13:12:49 -06:00
Dan Sosedoff 9c1ce91de1 Scope activity list to current database 2018-12-13 12:14:10 -06:00
Dan Sosedoff 073bcea56f Add note about pgweb pro version 2018-12-12 21:08:34 -06:00
Dan SosedoffandGitHub 9e3872c980 Merge pull request #396 from sosedoff/show-pgweb-version
Show current release and check for updates
2018-12-12 20:52:20 -06:00
Dan Sosedoff 658d14314b Show pgweb version on homep age and check for newer releases 2018-12-12 20:35:07 -06:00
Dan SosedoffandGitHub 107a3ac6d4 Merge pull request #395 from sosedoff/fix-bookmark-switch
Force switch back to default connection settings view
2018-12-12 20:19:37 -06:00
Dan Sosedoff ad82841eb2 Force switch back to default connection settings view 2018-12-12 18:29:07 -06:00
Dan SosedoffandGitHub c9d94b54d7 Merge pull request #394 from sosedoff/fix-row-estimation
Fix row estimation bug
2018-12-12 14:33:06 -06:00
Dan SosedoffandGitHub d96ddf9b18 Merge pull request #393 from sosedoff/debug-failed-queries
Print out failed query SQL and args with --debug flag
2018-12-12 13:57:17 -06:00
Dan Sosedoff 7917c2ad35 Simplify TableRowsCount func logic 2018-12-12 13:56:08 -06:00
Dan Sosedoff 2db5ce544f Fix row estimation bug
The variable `table` is mutated from `schema.tablename` to `tablename`
before being passed to other functions, which try to parse the schema
from the name. This results in schema name being `public` because it's
missing from the given table name.
2018-12-12 13:33:55 -06:00
Dan Sosedoff 474b99c6d5 Print out failed query SQL and args with --debug flag 2018-12-12 13:22:36 -06:00
Dan SosedoffandGitHub aded63f76f Merge pull request #390 from sosedoff/refactor2
Cleanup and refactor
2018-12-04 12:54:17 -06:00
Dan Sosedoff d1a7e6ecfe More lint 2018-12-04 12:42:37 -06:00
Dan Sosedoff a5b8e02a98 Cleanup backend handler 2018-12-01 22:35:11 -06:00
Dan Sosedoff 72af00f1b7 Refactor api package 2018-11-30 21:40:28 -06:00
Dan Sosedoff b86a849e00 Update pgweb version in dockerfile to 0.10.0 2018-11-28 10:39:02 -06:00
Dan Sosedoff 09c3a1862a Update changelog 2018-11-28 10:38:42 -06:00
Dan Sosedoff 54da27ef70 Release 0.10.0 2018-11-28 10:25:37 -06:00
Dan Sosedoff d9728cbb7a Changelog 2018-11-27 17:50:47 -06:00
Dan SosedoffandGitHub 420478ce6e Merge pull request #389 from sosedoff/fix-startup-with-default-db
Fix startup behavior when user did not provide a database name
2018-11-27 17:32:16 -06:00
Dan Sosedoff 21d2893292 Fix startup behavior when user did not provide a database name 2018-11-27 17:23:55 -06:00
Dan SosedoffandGitHub 1189bdef33 Merge pull request #388 from sosedoff/fix-json-naan-panic
Fix JSON marshal panic when dealing with NaN values
2018-11-27 17:00:38 -06:00
Dan SosedoffandGitHub 15d2e7917a Merge pull request #387 from sosedoff/url-prefix-env-var
Allow settings url prefix with URL_PREFIX env var
2018-11-27 17:00:10 -06:00
Dan Sosedoff 903a265676 Fix JSON marshal panic when dealing with NaN values 2018-11-27 16:51:51 -06:00
Dan Sosedoff 965cf754cf Allow settings url prefix with URL_PREFIX env var 2018-11-27 14:58:50 -06:00
Dan Sosedoff 3850eedf07 Add docker badge 2018-11-19 18:18:29 -06:00
Dan SosedoffandGitHub 0b400b1ffd Merge pull request #386 from HaraldNordgren/go_versions
Bump Go versions and use '.x' to always get latest patch versions
2018-11-14 18:40:21 -06:00
Dan Sosedoff abb143601b Add test for serverResult func 2018-11-05 17:07:32 -06:00
Harald Nordgren 4d193f0dd6 Bump Go versions and use '.x' to always get latest patch versions 2018-10-28 13:33:16 +01:00
Dan SosedoffandGitHub 9ca959c31c Merge pull request #381 from sosedoff/row-context-menu
Table row context menu
2018-09-16 16:42:51 -05:00
Dan Sosedoff 29d4b276e2 Do not show row context menu if results are empty 2018-09-14 18:19:24 -05:00
Dan Sosedoff 6ed3d5b501 Add result row context menu to copy value or filter rows 2018-09-14 18:14:18 -05:00
Dan Sosedoff 14037ddad2 Take out failing test due to new sessions behavior 2018-09-14 01:00:47 -05:00
Dan Sosedoff 1ecf18e41c Also flush bookmark under sessions/connect setup 2018-09-14 00:56:16 -05:00
Dan Sosedoff badcf700b9 Clear out connection settings when running in session/connect mode 2018-09-14 00:54:42 -05:00
Dan SosedoffandGitHub 527c680e1f Merge pull request #380 from sosedoff/localhost-default
Make localhost to be a default db host
2018-09-14 00:14:35 -05:00
Dan Sosedoff 174b2ffdbb Make localhost to be a default db host 2018-09-14 00:04:02 -05:00
Dan SosedoffandGitHub b4e2f2f13e Merge pull request #379 from sosedoff/numeric-stats-count
Include rows count to numeric stats view on table column
2018-09-14 00:00:11 -05:00
Dan Sosedoff fb7676ab4e Include rows count to numeric stats view on table column 2018-09-13 23:46:33 -05:00
Dan Sosedoff c9018e843e Regenerate bindata 2018-09-13 23:41:59 -05:00
Dan SosedoffandGitHub 31db9658e0 Merge pull request #377 from sosedoff/column-numstats
Add column context menu item to get numeric stats
2018-09-13 23:32:43 -05:00
Dan SosedoffandGitHub 449254bcfd Merge pull request #378 from sosedoff/connection-string-fixes
Fix issues with connection string builder
2018-09-13 23:31:06 -05:00
Dan Sosedoff fc380df8dd Fix failing win test 2018-09-13 23:22:25 -05:00
Dan Sosedoff dc4e8598f7 Refactor building connection string from options 2018-09-13 22:44:11 -05:00
Dan Sosedoff b0151ee985 Refactor connection string generator 2018-09-13 22:25:15 -05:00
Dan Sosedoff b9eba7bfa4 Add column context menu item to get numeric stats 2018-09-13 20:50:33 -05:00
Dan Sosedoff cad6c39e72 Update root package 2018-09-05 10:43:56 -05:00
Dan Sosedoff 2e64b56d14 Specify metadata for heroku 2018-09-05 10:35:45 -05:00
Dan SosedoffandGitHub 1620f2be56 Merge pull request #375 from sosedoff/dep
Dependencies update and switch to dep
2018-09-04 09:02:09 -05:00
Dan Sosedoff f6fa44b7fc Update go test script 2018-08-31 22:37:24 -05:00
Dan Sosedoff ab33472bf4 Turn off CGO for win 2018-08-31 22:30:59 -05:00
Dan Sosedoff 5ad15326ec Another appveyor pass 2018-08-31 22:24:36 -05:00
Dan Sosedoff 86c301bcf9 Try appveyor without overriding GOPATH 2018-08-31 22:19:48 -05:00
Dan Sosedoff 7a52df61ec Add missing dep install 2018-08-31 22:13:01 -05:00
Dan Sosedoff 0c40f1907e Update appveyor build script 2018-08-31 22:11:54 -05:00
Dan Sosedoff 35641e5957 Remove failing mime types from test 2018-08-31 22:01:51 -05:00
Dan Sosedoff 82130ecce0 Update CI matrix to use latest 2 versions of go 2018-08-31 21:57:48 -05:00
Dan Sosedoff ad81f666a5 Switch to dep for dependency management 2018-08-31 21:49:24 -05:00
Dan Sosedoff 847f47c5d3 Remove premium/donations section from readme 2018-08-20 21:18:49 -05:00
Dan Sosedoff aefcf81bb3 Changelog update 2018-08-20 21:15:50 -05:00
Dan Sosedoff e3b2d08021 Update dockerfile 2018-08-20 21:04:10 -05:00
Dan SosedoffandGitHub 3da2af9111 Merge pull request #370 from sosedoff/table-filter-tweaks
Automatically set table filter option to 'equals' if its not set
2018-08-20 20:54:16 -05:00
Dan Sosedoff a6eade1ae1 Automatically set table filter option to 'equals' if its not set 2018-08-20 20:42:38 -05:00
Dan SosedoffandGitHub 3b3d739e91 Merge pull request #356 from ericdagenais/itn/table-quotes
Fixes relation not found errors when dealing with table names that have uppercase characters
2018-06-26 22:23:48 -05:00
Eric Dagenais 9bb3afe6ce Merge remote-tracking branch 'upstream/master' into itn/tables-quotes-2
# Conflicts:
#	pkg/client/client.go
2018-06-20 07:19:34 -07:00
Dan SosedoffandGitHub 41a99be4ca Merge pull request #366 from allisson/master
Add EstimatedTableRowsCount to avoid count in large tables
2018-06-18 10:26:56 -05:00
Allisson Azevedo 808f6eea33 Change EstimatedTableRowsCount query 2018-06-16 18:12:11 -03:00
Allisson Azevedo 974570c79e Add more tests 2018-06-16 11:09:37 -03:00
Allisson Azevedo 5ba53d3cbd Add test_EstimatedTableRowsCount 2018-06-14 17:51:16 -03:00
Allisson Azevedo 8748f07167 Use EstimatedTableRowsCount only for postgresql 2018-06-14 16:36:22 -03:00
Allisson Azevedo d603b293d8 Add EstimatedTableRowsCount to avoid count in large tables 2018-06-14 16:10:59 -03:00
Dan SosedoffandGitHub 2ce5e9a1e2 Merge pull request #365 from sosedoff/cockroachdb
CockroachDB initial fixes
2018-06-05 22:59:02 -05:00
Dan Sosedoff 108754a8c3 Verify that database name is specified when opening the connection 2018-06-05 22:31:22 -05:00
Dan Sosedoff 8ea8960696 Fix killproc func in cockroach smoke test 2018-06-05 18:58:38 -05:00
Dan Sosedoff 1cbcb73948 Print a correct database server name when starting 2018-06-05 18:28:22 -05:00
Dan Sosedoff e68c7e2c61 Add cockroachdb smoke test 2018-06-05 18:20:58 -05:00
Dan Sosedoff e09f892442 Fix table info call, just return placeholder for now 2018-06-05 16:38:00 -05:00
Dan Sosedoff 56f3b45a4e Fix activity list 2018-06-05 15:48:16 -05:00
Dan Sosedoff 77770112bd Detect server type and version with regular expression 2018-06-05 15:35:19 -05:00
Dan SosedoffandGitHub b18af0b907 Merge pull request #358 from sosedoff/check-for-pgdump
Check if pg_dump is available before running database export
2018-04-26 00:14:17 -05:00
Dan Sosedoff da98261d27 Remove GOVENDOREXPERIMENT and add go 1.10.1 2018-04-26 00:08:30 -05:00
Dan Sosedoff 9fae7c06af Use -race flag with go test 2018-04-26 00:05:32 -05:00
Dan Sosedoff b29c013706 Use go get -u to fetch initial deps 2018-04-26 00:04:48 -05:00
Dan Sosedoff 0b095f43bb Restructure routes 2018-04-25 23:52:22 -05:00
Dan Sosedoff e2be689a05 Check if pg_dump is available before running database export 2018-04-25 23:29:18 -05:00
Dan Sosedoff 7dce0fb4f4 Cleanup whitespace in dockerfile 2018-04-25 23:19:39 -05:00
Dan SosedoffandGitHub a66cf92bd5 Merge pull request #357 from Bizzelicious/patch-1
Update Dockerfile
2018-04-25 23:19:00 -05:00
Jesper EnebergandGitHub 5c8bdf04a1 Update Dockerfile
* Changed MAINTAINER instruction to LABEL since MAINTAINER is deprecated. See https://docs.docker.com/engine/reference/builder/#maintainer-deprecated for more info.
2018-04-26 00:06:18 +02:00
Jesper EnebergandGitHub 315bf650d4 Update Dockerfile
* Removed `apk update`.
* Added `--no-cache` parameter. As of Alpine Linux 3.3 we can utilize the `--no-cache` option for apk. It allows us to install packages with an index that is updated and used on-the-fly and not cached locally. No need to run `apk update`, `apk add --update` and remove `/var/cache/apk/*` when done installing packages.
* Added postgres package so we can utilize pg_dump to dump tables and databases.
2018-04-26 00:00:05 +02:00
Dan Sosedoff f2579a7b37 Bump the pgweb version in docker file 2018-04-24 11:40:03 -05:00
Dan Sosedoff 319b88cedf Update changelog 2018-04-23 12:13:38 -05:00
Dan Sosedoff 9af721176b Release 0.9.12 2018-04-23 12:07:53 -05:00
Dan Sosedoff 113e2f27fb Regenerate bindata 2018-04-23 11:16:02 -05:00
Eric DagenaisandEric Dagenais 7b3e99b599 fixes issue that generates an error message when getting table info, getting table schema, and exporting data for tables with uppercase letters 2018-04-11 06:24:56 -07:00
Dan Sosedoff 2398035e53 Another pass 2018-03-16 20:15:06 -05:00
Dan Sosedoff d153d47604 Another pass at gofmt enforcement 2018-03-16 20:04:08 -05:00
Dan Sosedoff a02c813784 Check files with go fmt in CI 2018-03-16 19:59:58 -05:00
Dan SosedoffandGitHub d72f1c85c1 Merge pull request #347 from sosedoff/specs-refactor
Specs refactor and tweaks
2018-02-28 22:55:45 -06:00
Dan Sosedoff 62fc8a1054 Remove boot2docker mention, its deprecated and no longer supported 2018-02-28 22:35:02 -06:00
Dan Sosedoff e4b1c60ce5 Skip running tests on 10.x branch for now 2018-02-28 22:33:14 -06:00
Dan Sosedoff b620d8f0f2 Specify postgres 10.0 in test-all script 2018-02-22 14:41:45 -06:00
Dan Sosedoff 47500bf92e Specs refactor and tweaks
- Make ParseOptions func to take an arg and return struct so we can test it
- Refactor and add more tests for options parsing
- Run test suite on postgres 10.x branch
- Change format for connection idle timer from float64 to in
2018-02-22 14:20:18 -06:00
Dan Sosedoff 6938eb5f35 Update go versions for testing 2018-02-19 20:37:28 -06:00
Dan Sosedoff 8936adbe1c Merge branch 'go193' 2018-01-30 23:30:12 -06:00
Dan SosedoffandGitHub 121f278713 Merge pull request #343 from sosedoff/list-constraint-name
Include constraint name under "constraints" tab
2018-01-30 23:29:54 -06:00
Dan SosedoffandGitHub 0a0b07a255 Merge pull request #337 from ribice/patch-1
Update bookmarks.go
2018-01-30 23:22:33 -06:00
Dan Sosedoff f44b436bc1 Update travis config per recommendation 2018-01-30 23:20:19 -06:00
Dan Sosedoff 75ab487031 Go 1.9.3 on travis 2018-01-30 22:51:52 -06:00
Dan Sosedoff 133c936d7b License years 2018-01-30 22:42:37 -06:00
Dan Sosedoff b0146a5a76 Update test case to handle the constraint result rows 2018-01-30 22:41:12 -06:00
Dan Sosedoff 9c0d887a3c List name and definition for table constraints 2018-01-30 22:34:54 -06:00
Emir RibićandGitHub 2b2c76bde9 Update bookmarks.go
Fix typo
2018-01-08 10:55:05 +01:00
Dan Sosedoff 43fe84de3a Add link to PG docs about connection strings 2017-12-14 22:49:10 -06:00
Dan Sosedoff 318c33e8ff Changelog 2017-12-07 23:06:28 -06:00
Dan Sosedoff c32784e8ae Release 0.9.11 2017-12-07 22:57:56 -06:00
Dan Sosedoff 898cdd2d65 Bindata gen 2017-12-07 22:56:55 -06:00
Dan SosedoffandGitHub b441151e09 Merge pull request #330 from sosedoff/view-actions
Add views context menu to sidebar
2017-12-01 09:52:21 -06:00
Dan Sosedoff 0ea173ffe5 Add views context menu 2017-11-30 23:38:15 -06:00
Dan SosedoffandGitHub 4b1d0a614c Merge pull request #329 from sosedoff/db-prefix-for-export-files
Add db prefix for table export files
2017-11-30 22:57:44 -06:00
Dan Sosedoff 02d598f406 Add db prefix for table export files 2017-11-30 22:47:42 -06:00
Dan SosedoffandGitHub ae11e469d2 Merge pull request #327 from sosedoff/no-autocomplete-on-connection-url
Disable autocomplete on the connection url string for security reasons
2017-11-30 22:35:05 -06:00
Dan Sosedoff 4f2fac11d3 Disable autocomplete on the connection url string for security reasons 2017-11-30 22:25:20 -06:00
Dan Sosedoff 68bc907bd2 Godep: 1.9 2017-11-28 19:37:25 -06:00
Dan Sosedoff d7e5805024 Rename SQL query tab 2017-11-21 15:33:13 -06:00
Dan SosedoffandGitHub 97e31b6c47 Merge pull request #321 from sosedoff/cors
Add support for CORS
2017-11-18 21:49:07 -06:00
Dan SosedoffandGitHub cb67ef8258 Merge pull request #323 from sosedoff/table-context-reorg
Reorder table context menu
2017-11-16 23:18:39 -06:00
Dan Sosedoff dc3d7303d7 Reorder table context menu 2017-11-16 23:08:18 -06:00
Dan SosedoffandGitHub cdb74c6a19 Merge pull request #322 from sosedoff/fix-custom-results-rows
Hide custom query results counter if query didn't return any rows
2017-11-16 23:07:24 -06:00
Dan Sosedoff cf432e32f7 Hide custom query results counter if query didnt return any rows 2017-11-16 22:21:52 -06:00
Dan Sosedoff c3435e2ece Change CORS to Cors on CLI options, specify origins as an option 2017-11-16 22:10:14 -06:00
Dan Sosedoff b52394a166 Add support for CORS 2017-11-15 15:26:31 -06:00
Dan SosedoffandGitHub d65f8eb0f5 Merge pull request #320 from tylerlong/master
Fix sslmode typo
2017-11-11 10:58:59 -06:00
Tyler LongandGitHub 86acd842b0 Fix sslmode typo
https://www.postgresql.org/docs/9.1/static/libpq-ssl.html
2017-11-11 08:12:27 -06:00
Dan Sosedoff 69b89a0c2a Update docker image version 2017-11-03 22:05:46 -05:00
Dan Sosedoff 2992dd43e8 Update changelog 2017-11-03 22:05:29 -05:00
Dan Sosedoff cb4ecb49eb Release 0.9.10 2017-11-03 21:50:25 -05:00
Dan Sosedoff d51294ce54 Rebuild bindata 2017-11-03 21:49:32 -05:00
Dan SosedoffandGitHub 32eb35f7b2 Merge pull request #311 from krlevkirill/patch-1
Update README - add more badges
2017-10-30 20:52:10 -05:00
krlevkirillandGitHub 80717e6c69 Update README
add more badges
fix link to license
2017-10-29 20:07:28 +02:00
Dan SosedoffandGitHub d123f8fc07 Merge pull request #305 from sosedoff/assets-ci-guard
Trigger CI failure if bindata contains any dev links
2017-10-25 09:39:30 -05:00
Dan Sosedoff 2dcaf4a3a1 Remove bindata check from test-all script 2017-10-24 23:13:22 -05:00
Dan Sosedoff c4cdacc2d8 Add asset check script 2017-10-24 23:11:02 -05:00
Dan Sosedoff 06afa786ef Trigger CI failure if bindata contains any dev links 2017-10-24 22:38:51 -05:00
Dan Sosedoff b8fccab33a Fix broken bindata 2017-10-24 22:31:29 -05:00
Dan Sosedoff ec5a12b0f7 Rename disable connection timeout cli option 2017-10-20 22:18:07 -05:00
Dan SosedoffandGitHub 7b6c5f74d4 Merge pull request #303 from ldinc/issue#282
Issue#282
2017-10-20 22:14:26 -05:00
ldinc 02df9b519f #282: cleared bindata 2017-10-16 00:12:41 +03:00
ldinc 38a3f01868 #282: set flag idle logic to cli-run phase 2017-10-15 01:39:07 +03:00
ldinc a120b58dc2 #282: updated bindata 2017-10-14 14:37:20 +03:00
ldinc 33be6b036d Merge branch 'master' into issue#282 2017-10-14 14:34:32 +03:00
Dan Sosedoff 93211cf073 Merge branch 'EyesBear-master' 2017-10-13 23:04:52 -05:00
Dan Sosedoff 046022221e UI and design tweaks 2017-10-13 23:04:19 -05:00
Jason YeandDan Sosedoff fc01b1db81 Search for database with autocomplete 2017-10-13 22:59:24 -05:00
Dan Sosedoff ed7557c5da Regenerated bindata 2017-10-13 22:34:16 -05:00
Dan SosedoffandGitHub 5d110da392 Merge pull request #302 from ldinc/issue#301
added simple copy to clipboard from data-id prop
2017-10-13 22:33:58 -05:00
ldinc ab443f58d0 #301: change rexexp to String.split() 2017-10-14 03:20:42 +03:00
ldinc d4dde3fe4f #282 fix disabled state per idle timeout 2017-10-14 03:15:31 +03:00
ldinc 3067b06bf6 #282: added timeout flags & tuned client.IsIdle() 2017-10-14 03:08:04 +03:00
ldinc 0e2926cc85 added simple copy to clipboard from data-id prop 2017-10-14 01:23:53 +03:00
Dan SosedoffandGitHub 72b9eb443c Merge pull request #300 from sosedoff/disable-bookmark-on-startup
Dot now allow using startup bookmark in multi-session mode
2017-10-07 12:33:10 -05:00
Dan Sosedoff 4ad70d5392 Tweak wording 2017-10-07 11:30:12 -05:00
Dan Sosedoff d8ce8231cc Dot now allow using startup bookmark in multi-session mode 2017-10-07 09:25:00 -05:00
Dan SosedoffandGitHub 7381ff8589 Merge pull request #296 from sosedoff/ssh-hotfix
Fix panics when sshinfo is not set on bookmarks
2017-10-04 00:31:35 -05:00
Dan Sosedoff 277e2d6cd8 Add an extra test to verify SSHinfoIsEmpty does not panic 2017-10-03 23:03:02 -05:00
Dan Sosedoff 58ae69de0b Fix panics when sshinfo is not set on bookmarks 2017-10-03 23:00:18 -05:00
Dan Sosedoff f73f812cf1 Correction on idle timeout 2017-09-29 00:06:48 -05:00
Dan Sosedoff d1eba4a089 Update docker image version 2017-09-28 21:03:55 -05:00
Dan Sosedoff 502c13c3ce Fix formatting 2017-09-28 20:58:47 -05:00
Dan Sosedoff ee9878ac25 Update changelog 2017-09-28 20:56:24 -05:00
Dan Sosedoff 4cacfc253c Release 0.9.9 2017-09-28 20:42:14 -05:00
Dan SosedoffandGitHub b6167ce422 Merge pull request #279 from sosedoff/disable-ssh
Add new CLI option to disable SSH connections
2017-09-27 00:35:38 -05:00
Dan Sosedoff b1dd4b5308 Add new CLI option to disable SSH connections 2017-09-26 23:56:27 -05:00
Dan SosedoffandGitHub e7a7e02626 Merge pull request #277 from sosedoff/fix-ssh-connections
SSH Connection Fixes
2017-09-26 23:50:12 -05:00
Dan Sosedoff 5e22795828 Allow setting ssh connection key file, fix connection ui 2017-09-26 23:29:28 -05:00
Dan Sosedoff bf423658af Rename examples to config, it's a proper name 2017-09-26 22:12:01 -05:00
Dan SosedoffandGitHub 8bd6f08794 Merge pull request #275 from sosedoff/close-idle-sessions
Automatically close idle sessions
2017-09-22 23:01:01 -05:00
Dan Sosedoff 62d88121cd Automatically close idle sessions 2017-09-22 22:44:32 -05:00
Dan Sosedoff 85e38c390c Merge branch 'dump-test' 2017-09-22 22:17:29 -05:00
Dan Sosedoff 10b6e69dec Do not run CI on go 1.5/1.6 2017-09-22 22:08:32 -05:00
Dan Sosedoff 5d1cc211b0 Add database export test 2017-09-22 22:06:36 -05:00
Dan SosedoffandGitHub 83598f4013 Merge pull request #266 from sosedoff/connect-backend
Add ability to connect with settings from third-party backend
2017-09-21 20:37:46 -05:00
Dan SosedoffandGitHub 76eaa3bc4b Merge pull request #272 from sosedoff/auto-open-when-running
Automatically open pgweb page if its already running
2017-09-21 20:35:21 -05:00
Dan Sosedoff 92ec968d5e Automatically open pgweb page if its already running 2017-09-21 17:16:09 -05:00
Dan Sosedoff 049aa4ec44 Require --connect-backend flag to be set if any other backend flags are specified 2017-09-21 02:06:14 -05:00
Dan Sosedoff 869fd8c6bc Refactor the third-party connect backend functionality 2017-09-21 01:21:26 -05:00
Dan Sosedoff d175b0af34 Add descriptions for cli backend options 2017-09-19 00:45:07 -05:00
Dan Sosedoff 0aff72059b Check for response status when fetching credentials 2017-09-19 00:43:21 -05:00
Dan Sosedoff 168b33d5d5 Test on go1.9 2017-09-19 00:43:21 -05:00
Dan Sosedoff ccb84f585a Fix deps again 2017-09-19 00:43:21 -05:00
Dan Sosedoff 39e7b3f615 Update deps 2017-09-19 00:43:21 -05:00
Dan Sosedoff 73816ff2d7 Add ability to connect with settings from third-party backend 2017-09-19 00:43:21 -05:00
Dan SosedoffandGitHub f5f595ac02 Merge pull request #270 from sosedoff/data-export
Database export
2017-09-19 00:41:04 -05:00
Dan SosedoffandGitHub c0a4e7add1 Merge pull request #271 from sosedoff/pretty-json-default
Fix the JSON beautifier flag
2017-09-16 22:11:41 -05:00
Dan Sosedoff 26ea9fc1b8 Fix the JSON beautifier flag 2017-09-16 21:57:04 -05:00
Dan Sosedoff 3c515fcef3 Update readme: premium features/donations 2017-09-16 21:28:06 -05:00
Dan Sosedoff e771d6f49e Export database or table to sql 2017-09-16 20:32:41 -05:00
Dan SosedoffandGitHub 003a661c08 Merge pull request #268 from sosedoff/fetch-unique-rows
Table headers context menu
2017-09-16 14:22:10 -05:00
Dan Sosedoff 28b0840e69 Move some of the util functions to util.js 2017-09-16 14:03:42 -05:00
Dan Sosedoff 81af2a9a27 Add table results headers context menu 2017-09-16 13:50:22 -05:00
Dan SosedoffandGitHub 206416889c Merge pull request #265 from sosedoff/connection-usage
Record last query timestamp
2017-09-15 17:03:31 -05:00
Dan Sosedoff 6ab30ed308 Update the client last usage timestamp 2017-09-14 00:16:40 -05:00
Dan SosedoffandGitHub 3a9bb58646 Merge pull request #264 from sosedoff/print-version-on-start
Print postgres version on start in a single-session mode
2017-09-14 00:00:21 -05:00
Dan Sosedoff af7e504e4f Print postgres version on start in a single-session mode 2017-09-13 23:42:44 -05:00
Dan SosedoffandGitHub 2178a507c5 Merge pull request #255 from sosedoff/pretty-json-default
Pretty-print JSON results by default
2017-09-13 23:42:19 -05:00
Dan SosedoffandGitHub 7832b5b8b8 Merge pull request #256 from sosedoff/docker-alpine-3.6
Use alpine:3.6 as a docker base image
2017-09-13 23:36:33 -05:00
Dan Sosedoff d786d0018e Make JSON formatting a feature flag, enabled by default 2017-09-13 23:27:54 -05:00
Dan Sosedoff a9efb88fe2 Use alpine:3.6 as a docker base image 2017-08-09 23:11:30 -05:00
Dan Sosedoff e44e66a8e8 Use indent marshaller for JSON results by default 2017-08-04 22:10:12 -05:00
Dan Sosedoff 7d81998577 Update changelog and docker image 2017-08-04 21:58:43 -05:00
Dan Sosedoff 5e748d0cef Release 0.9.8 2017-08-04 21:44:22 -05:00
Dan Sosedoff 4f90561c41 Regenerate bindata 2017-08-04 21:41:15 -05:00
Dan SosedoffandGitHub dc57d5d15f Merge pull request #254 from skabbes/feature/comments
Expose postgres comments to table and columns
2017-08-04 19:40:04 -05:00
Steven Kabbes 2d03aa02e2 test: attempt to fix tests again 2017-08-04 14:16:59 -07:00
Steven Kabbes 8d4d564935 test: fix tests to expect comment column 2017-08-04 14:13:49 -07:00
Steven Kabbes e710dbf720 Add comments to tables and columns 2017-08-04 14:00:52 -07:00
Dan SosedoffandGitHub aba81334a5 Merge pull request #246 from sosedoff/confirm-disconnect
Require confirmation to close the connection
2017-06-05 23:15:31 -05:00
Dan SosedoffandGitHub c53c974713 Merge pull request #247 from sosedoff/postgres-9.6-ci
Test primarily on postgres 9.6
2017-06-05 23:15:17 -05:00
Dan Sosedoff 574f642200 Require confirmation to close the connection 2017-06-05 23:15:03 -05:00
Dan SosedoffandGitHub fd6e4cc189 Merge pull request #245 from sosedoff/for-of-fix
Javascript fixes for IE9-11
2017-06-05 22:46:03 -05:00
Dan Sosedoff c8b90d1286 Test primarily on postgres 9.6 2017-06-05 22:44:57 -05:00
Dan SosedoffandGitHub cbf50299ad Merge pull request #244 from sosedoff/bookmark-ssl-validation
Bookmarks fall back to sslmode=disable if mode is not set or invalid
2017-06-05 22:41:35 -05:00
Dan Sosedoff 26b2cdc194 Fix js on IE9-11 2017-06-05 22:29:44 -05:00
Dan Sosedoff 00ee7084a6 Specify how to run all tests 2017-06-05 21:30:53 -05:00
Dan Sosedoff 75dc1c1548 Fall back to sslmode=disable if ssl mode is not set or invalid in bookmarks 2017-06-05 21:29:09 -05:00
Dan Sosedoff 943963f760 Fix test for mime x-font-woff, pin down golang versions in travis 2017-06-05 21:08:56 -05:00
Dan SosedoffandGitHub 03d3c35a86 Merge pull request #240 from sosedoff/paginate-remember-sort-order
Remember sort column and order for pagination
2017-05-16 22:52:02 -05:00
Dan Sosedoff 3ed194b7bc Remember sort column and order for pagination 2017-05-16 22:34:58 -05:00
Dan Sosedoff c764d53b88 Regenerate bindata 2017-05-16 21:28:24 -05:00
Dan Sosedoff ccc5eddf92 Move all CLI logic into its own package 2017-05-16 21:28:07 -05:00
Dan SosedoffandGitHub 1ff2f7d7ba Merge pull request #238 from bjeanes/patch-1
Show query waiting for 9.6 in activity section
2017-05-10 22:40:49 -05:00
Bo JeanesandGitHub c3f518e5b7 Show query waiting for 9.6 in activity section 2017-05-11 13:30:16 +10:00
Dan SosedoffandGitHub 84cfef8480 Merge pull request #237 from sosedoff/activity-fix
Activity tab fix
2017-05-10 22:14:21 -05:00
Dan Sosedoff bf14066aba Alias procid column to pid so that frontend can properly read it 2017-05-09 23:01:10 -05:00
Dan Sosedoff c592d0063d Set activity queries for different pg versions 2017-05-09 22:51:26 -05:00
Dan Sosedoff e9787440fb Set server version for the client on start up 2017-05-09 21:53:24 -05:00
Dan Sosedoff 1ee02d1451 Add activity query client test 2017-05-09 21:53:24 -05:00
Dan Sosedoff 2378ba894d Reset results table on manual disconnect 2017-05-08 21:37:24 -05:00
Dan SosedoffandGitHub 0fd652f716 Merge pull request #234 from tamird/cockroach
api: properly check for errors
2017-04-30 11:25:38 -05:00
Tamir Duberstein c4a33206ce api: properly check for errors
This results in NPE when cl.Info() returns an error.
2017-04-29 16:56:31 -04:00
Dan SosedoffandGitHub 6289e479a5 Merge pull request #232 from alanfran/patch-1
Removed duplicate usage line in Makefile.
2017-04-27 08:30:15 -05:00
Antonio LanfranchiandGitHub 7d97f6424b Removed duplicate usage line in Makefile.
There were two lines for `make test`.
2017-04-27 05:16:10 -04:00
Dan Sosedoff 383d4049e0 Build docker image on 0.9.7 2017-04-10 10:17:03 -05:00
Dan Sosedoff 5fc844b840 Update changelog for 0.9.7 2017-04-04 22:16:39 -05:00
Dan Sosedoff 4833a76b05 Version bump: 0.9.7 2017-04-04 22:10:45 -05:00
Dan Sosedoff f594dd77fd Update bindata 2017-04-04 22:10:26 -05:00
Dan Sosedoff f99afad7f6 Show build status on linux based off master 2017-04-04 22:06:00 -05:00
Dan Sosedoff 1c327ac252 Use postgres 9.5 by default on travis 2017-02-27 11:52:18 -06:00
Dan Sosedoff 7556726e08 Test on Go 1.8 2017-02-26 15:55:45 -06:00
Dan SosedoffandGitHub 92729f26df Merge pull request #225 from sosedoff/reset-query-rows
Remove table rows when switching to custom query view
2017-02-25 14:43:07 -06:00
Dan Sosedoff 701e536a90 Remove table rows when switching to custom query view 2017-02-25 14:24:57 -06:00
Dan SosedoffandGitHub 1f45a95710 Merge pull request #224 from sosedoff/result-rows-count
Show number of rows rendered for custom sql query
2017-02-25 14:16:25 -06:00
Dan Sosedoff 9f7f83cfd2 Add example initd config 2017-02-25 14:15:03 -06:00
Dan Sosedoff 36fd4871e1 Show number of rows rendered for custom sql query 2017-02-25 14:04:48 -06:00
Dan Sosedoff 7882abea35 Update godeps 2017-02-03 20:29:16 -06:00
Dan SosedoffandGitHub 67fbadabf2 Merge pull request #217 from sosedoff/deps-update
Deps update
2017-01-24 11:16:55 -06:00
Dan SosedoffandGitHub a4f2553fbc Merge pull request #218 from kpashka/master
Ability to override bookmarks directory + typo fix
2017-01-23 19:25:34 -06:00
Pavel Kiselev 4704a8a234 Override in tests 2017-01-24 08:06:12 +07:00
Pavel Kiselev ee547c5899 Override parameter in main.go 2017-01-24 08:04:13 +07:00
Pavel Kiselev 9b1d915dd1 Ability to override bookmarks directory 2017-01-24 07:55:18 +07:00
Dan SosedoffandGitHub f6f40ab707 Merge pull request #214 from jeasoft/patch-1
Example Configuration File for Ubuntu 14.04
2017-01-20 14:15:49 -06:00
Dan Sosedoff 33f3e20849 Use port 30000 to test port availability so it does not interfere with running pgweb instance 2017-01-20 13:41:24 -06:00
Dan Sosedoff 4fcb946f0f Update dependencies 2017-01-20 13:21:18 -06:00
Dan Sosedoff 12976024b7 Update go version in godeps to 1.7 2017-01-20 13:13:00 -06:00
Dan Sosedoff 1f7367edb1 Update license years 2017-01-20 13:05:18 -06:00
Jearel AlcantaraandGitHub 0493d77f46 Example Configuration File for Ubuntu 14.04
Example Configuration File for Ubuntu 14.04 and Upstart, normally in:
/etc/init/
2017-01-12 20:55:50 -04:00
Dan Sosedoff 4af979ac9b Fix issue with long database names in header 2017-01-10 13:44:42 -06:00
Dan SosedoffandGitHub c28695fa18 Merge pull request #212 from fduraffourg/SwitchDb
Fix path rewrite on DB change
2017-01-10 10:49:00 -06:00
Florian Duraffourg 5ffd6d612b Fix path rewrite on DB change 2017-01-10 16:14:50 +01:00
Dan SosedoffandGitHub 577edd86f4 Merge pull request #206 from sosedoff/fix-lock-session
Do not return error when session is locked and database url is blank
2016-11-20 16:55:13 -06:00
Dan Sosedoff 412089948c Do not return error when session is locked and database url is blank 2016-11-20 16:44:33 -06:00
Dan Sosedoff 65773364a1 Update docker version 2016-11-18 12:51:37 -06:00
Dan Sosedoff 91bc5d0d5e Update changelog 2016-11-18 12:40:31 -06:00
Dan Sosedoff b580b2456a Version bump: 0.9.6 2016-11-18 12:36:27 -06:00
Dan Sosedoff 7e27cfe5f8 Regenerate bindata 2016-11-17 21:43:52 -06:00
Dan Sosedoff 4609a3d5f3 Update changelog [ci-skip] 2016-11-15 22:19:24 -06:00
Dan SosedoffandGitHub 20a5646a53 Merge pull request #202 from sosedoff/always-show-sidedebar
Display empty sidebar
2016-11-15 22:06:18 -06:00
Dan Sosedoff 0510634db7 Move bookmark default port parsing to readServerConfig func 2016-11-15 22:01:42 -06:00
Dan Sosedoff 59018287de Fix issue with default port in bookmark; cleanup 2016-11-15 21:58:58 -06:00
Dan Sosedoff e1edc5b550 Resolve conflict with master 2016-11-15 21:51:23 -06:00
Dan SosedoffandGitHub d74bc06cbd Merge pull request #201 from akarki15/Issue-189-take-2
Issue-189 : Allow connecting to bookmarked server from CLI
2016-11-15 21:49:27 -06:00
akarki15 fedde804e3 Remove error return value from ConvertToOptions
ConvertToOptions can't return any error.
2016-11-15 22:03:32 -05:00
Dan Sosedoff 7467553bf2 Fix bindata 2016-11-15 20:55:17 -06:00
Dan Sosedoff 9f8f45db5a Display sidebar even if there are no tables (or any other objects) 2016-11-15 20:55:03 -06:00
akarki15 b1fdbf714b Make assets. 2016-11-12 13:19:09 -05:00
akarki15 68db934507 Use generic typed error pattern
The codebase doesn't yet use the explicitly typed error pattern. To keep
things consistent, lets use the generic error type.
2016-11-12 12:30:15 -05:00
akarki15 1b4902f196 initClient uses bookmark if it exists
if options.Bookmark is set, initClient will now use
it to create a client from the bookmarked settings.

initClientUsingBookmark uses methods introduced in
previous commits. It constructs connection string
to db from Bookmark.Url if it exists. If not, it
uses other fields in Bookmark to construct the
connection string. An implication of this is that
the Url field of Bookmark takes precedence.
2016-11-10 20:06:39 -05:00
akarki15 61dfe67ca8 Add a method to convert bookmark to options
We will use client.NewFromURL to create a client
from stored bookmark in next commit. Since
client.NewFromURL takes in a command.Options, lets
add a method on Bookmark to get corresponding Option.
2016-11-10 20:06:39 -05:00
akarki15 038cb620c6 Set the type of Bookmark.Port to int
We know that a port is a number. Lets enforce that
rule at type level by setting it so.

This commit also adjusts test funcs and helper data
to fit Port's new int type.
2016-11-10 20:06:39 -05:00
akarki15 0e88e3e1f4 Add a func to read bookmark from stored toml file
Given a bookmark path and bookmark name, GetBookmark
returns a Bookmark object that corresponds to the
stored bookmark settings.

In next commits, we will use this method to read
stored bookmarks and create a db client.
2016-11-10 20:06:39 -05:00
akarki15 f4fb5744ef Add method that returns if bookmark's ssh is empty
We will use this method in upcoming commit to create
client from stored bookmark.
2016-11-10 20:06:39 -05:00
Dan SosedoffandGitHub 474c439076 Merge pull request #198 from gallifabio/feature/favor-selected-text
Explain and Export: favor selected text
2016-11-09 10:10:44 -06:00
Fabio Galli 9cb75ec608 Updated bindata file to align with "Explain and Export: favor selected text" change 2016-11-09 09:53:34 +01:00
Dan Sosedoff 560308fd31 Update docker version to 0.9.5 2016-11-08 09:38:01 -06:00
Fabio Galli da9bdb8dcd Favor selected text over the whole ace editor text in "runExplain" and "Export" functionalities 2016-11-08 09:28:23 +01:00
Dan Sosedoff b3723e260f Typo fix 2016-11-07 18:57:25 -06:00
Dan Sosedoff c6bc631327 Add notes about upcoming release in the changelog 2016-11-07 18:56:35 -06:00
Dan SosedoffandGitHub ec013488ff Merge pull request #197 from sosedoff/sticky-tabs
Make tabs sticky: remember tab when switching between tables
2016-11-06 22:19:16 -06:00
Dan Sosedoff 58e5609b59 Make tabs sticky: remember tab when switching between tables 2016-11-06 22:09:46 -06:00
Dan SosedoffandGitHub 00c2b8262b Merge pull request #196 from sosedoff/switch-db
Allow switching between databases
2016-11-06 21:52:58 -06:00
Dan Sosedoff 172f9a82d0 Remove dummy data from database list and tweak the link style 2016-11-06 19:01:38 -06:00
Dan Sosedoff 72325a6cbc Show connect/disconnect button if connection info fetch failed 2016-11-05 22:51:10 -05:00
Dan Sosedoff a3c341b5b4 Show list of databases and allow switching by clicking on current database name 2016-11-05 22:49:17 -05:00
Dan Sosedoff 7d08017c7f Add endpoint to switch active database 2016-11-05 22:23:26 -05:00
Dan SosedoffandGitHub 07ee8448e6 Merge pull request #195 from sosedoff/session-lock
Implement session locking with --lock-session option
2016-11-05 21:58:56 -05:00
Dan Sosedoff 20da36416c Implement session locking with --lock-session option 2016-11-05 21:35:11 -05:00
Dan SosedoffandGitHub 97b612c1b3 Merge pull request #193 from sosedoff/read-only-mode
Read only mode
2016-11-05 21:29:30 -05:00
Dan SosedoffandGitHub 01a69b9bf9 Merge pull request #194 from sosedoff/kill-running-query
Allow stopping any running query from activity panel
2016-11-05 20:51:31 -05:00
Dan Sosedoff d9cb536518 Add security warning 2016-11-05 20:50:56 -05:00
Dan Sosedoff 887acd5a55 Allow stopping any running query from activity panel 2016-11-05 19:53:10 -05:00
Dan Sosedoff da0916b8c7 Add note about skipped windows tests 2016-11-05 18:53:42 -05:00
Dan Sosedoff 8f90ec4173 Add missing history uniqueness test 2016-11-05 17:52:18 -05:00
Dan Sosedoff 2d2bd1d0b1 Add test to verify readonly mode 2016-11-05 17:51:34 -05:00
Dan Sosedoff 661fed0dbb Set default transaction mode to read only with --readonly flag 2016-11-05 17:43:30 -05:00
Dan Sosedoff e3e1d7b012 Remove PGHOST from test-all script for travis 2016-11-04 10:33:37 -05:00
Dan SosedoffandGitHub 1e88d3dc5f Merge pull request #191 from sosedoff/order-escape-query
Add escaping to order statement
2016-11-03 20:08:55 -05:00
Dan SosedoffandGitHub b943f5a51d Merge pull request #190 from sosedoff/fix-pagination-visibility
Show pagination only on rows view
2016-11-03 20:07:46 -05:00
Dan Sosedoff 5b8937ac3a Deprecate Boot2Docker in favor of Docker for Mac for integration testing 2016-11-03 20:05:50 -05:00
Dan Sosedoff d0d84a62cd Add escaping to order statement 2016-11-03 19:56:55 -05:00
Dan Sosedoff 7aa7b5526b Show pagination only on rows view 2016-11-03 19:27:14 -05:00
Dan Sosedoff 812aff9686 Regenerate bindata 2016-10-11 18:12:39 -05:00
Dan SosedoffandGitHub 23e6b02ae4 Merge pull request #186 from SjonHortensius/js-export-btoa-fix
Fix base64 padding with multiple =-signs. An equal amount of dots is …
2016-10-11 18:12:10 -05:00
Sjon Hortensius 2f69538576 Fix base64 padding with multiple =-signs. An equal amount of dots is required in output
instead of merging all =-signs into one dot, which can then not be
decoded. Test string:

SELECT * from version  where "isHelper" order by "order" desc

will be encode to

U0VMRUNUICogZnJvbSB2ZXJzaW9uICB3aGVyZSAiaXNIZWxwZXIiIG9yZGVyIGJ5ICJvcmRlciIgZGVzYw.

missing a dot at the end making it undecodable.

Since =-signs only appear at the end anyway, we don't need the explicit
'$' in the regexp
2016-10-03 16:19:27 +02:00
Dan Sosedoff e52782fe9c Include make docker-release into CI pipeline 2016-10-01 20:43:20 -05:00
Dan Sosedoff 376d2283bf Fix compile tags in makefile due to compiler warnings 2016-10-01 20:32:05 -05:00
Dan Sosedoff 3c3c61fee2 Version bump: 0.9.5 2016-10-01 20:24:49 -05:00
Dan Sosedoff b6a7d8a54a Move systemd script to examples dir 2016-09-28 20:51:54 -05:00
Dan SosedoffandGitHub 9f4c803495 Merge pull request #177 from PSSGCSim/master
Create pgweb.service
2016-09-28 20:51:14 -05:00
Dan Sosedoff d4baaaa0ff Try travis go 1.7 2016-09-28 20:43:33 -05:00
Dan Sosedoff f3bc35ad35 Allow setting auth user and pass using variables 2016-09-28 20:37:07 -05:00
Dan Sosedoff ae8b87fb18 Regenerate bindata 2016-09-05 12:45:44 -05:00
Jan SchwarzrockandGitHub f0406cac8e Create pgweb.service
Add service file for Linux systemd
2016-09-04 11:19:22 +02:00
Dan Sosedoff 12dcc45e88 Formatting; regenerated bindata 2016-09-01 10:06:51 -05:00
Dan Sosedoff 1f39c2e229 Lint: Fix sql statements naming 2016-09-01 10:05:46 -05:00
Dan SosedoffandGitHub 9430d734a7 Merge pull request #175 from cchesk/cleanpr
Fixed broken export to CSV/JSON/XML if hashmark in URL
2016-09-01 10:05:43 -05:00
Dan SosedoffandGitHub 2ca5463d1d Merge pull request #167 from trengrj/limit-schema
Only view schema with USAGE privileges
2016-09-01 09:56:46 -05:00
Aleksander Umov a9fe99b09b Fixed broken export to CSV/JSON/XML if hashmark in URL 2016-08-30 10:50:47 +02:00
Dan Sosedoff dd71d1c49e Add go 1.7 to travis, bump 1.6 2016-08-16 09:46:18 -05:00
Dan Sosedoff ee93fbb120 Fix bindata 2016-08-01 10:32:22 -05:00
Dan Sosedoff ce96579d26 Update dockerfile 2016-07-29 10:43:23 -05:00
Dan Sosedoff 3ae6392a82 Version bump: 0.9.4 2016-07-29 10:37:18 -05:00
Dan Sosedoff ac16a94a92 Update bindata 2016-07-29 10:36:45 -05:00
Dan SosedoffandGitHub 65f5bb2daf Merge pull request #170 from skabbes/hotfix/export_prefix
[UI] fix CSV, JSON, and XML export with --prefix running
2016-07-29 10:21:14 -05:00
Steven Kabbes 22810a96b7 Update bindata 2016-07-27 22:59:58 -07:00
Steven Kabbes 638fd6234e [UI] fix CSV, JSON, and XML export with --prefix running 2016-07-27 22:55:54 -07:00
John Trengrove 838b275c19 Only view schema with USAGE privileges 2016-07-18 13:06:48 +10:00
Dan Sosedoff 6945843c97 Update changelog 2016-07-13 10:08:43 -06:00
Dan Sosedoff 6d53606b22 Update pgweb version in docker image 2016-06-30 10:35:06 -05:00
Dan Sosedoff 789719ada6 Version bump: 0.9.3 2016-06-30 10:27:37 -05:00
Dan SosedoffandGitHub 7014ab4d01 Merge pull request #165 from sosedoff/fix/csv-timestamp-formatting
Specify cell timestamp layout for CSV export, #163
2016-06-28 10:12:35 -05:00
Dan Sosedoff 790833dc4e Specify cell timestamp layout for CSV export, #163 2016-06-27 22:38:48 -05:00
Dan Sosedoff f575441aa6 Add support for ARMv5 2016-06-27 21:21:36 -05:00
Dan Sosedoff e4b13b753b Add tasks to build and push docker containers 2016-06-27 20:54:29 -05:00
Dan Sosedoff 76622dc32e Switch to alpine docker image, related to #164 2016-06-27 20:48:36 -05:00
Dan Sosedoff 80f2641bae Update bindata 2016-06-27 20:24:55 -05:00
Dan Sosedoff 186f132262 Tweak test directory 2016-06-27 19:52:26 -05:00
Dan Sosedoff 00a1ebaf25 Include postgres 9.6 to integration test 2016-06-15 11:40:01 -05:00
Dan Sosedoff 09389e2ee4 Merge pull request #155 from freeformz/update
Upgrade to Go1.6, use vendor
2016-05-26 21:00:28 -05:00
Edward Muller 5d2412bbb0 Upgrade to Go1.6, use vendor
Also make sure we're using a more recent version of godep
2016-05-26 16:37:46 -07:00
Dan Sosedoff 174a74023d Merge pull request #147 from ernadhalilovic/master
A small typo fix
2016-04-15 10:57:05 -05:00
Ernad Halilovic d607a76097 A small typo fix 2016-04-15 12:51:28 +02:00
Dan Sosedoff 34056c1c66 Version bump: 0.9.2 2016-03-01 19:48:15 -06:00
Dan Sosedoff c58138b4cc Do not reset connection form when clicking on 'cancel' button, fixes #138 2016-03-01 19:42:01 -06:00
Dan Sosedoff 1d50e2f0bf Merge pull request #143 from sosedoff/session-id-for-export
Session id for export
2016-02-26 12:49:10 -08:00
Dan Sosedoff c57b477dc9 Add tests for getSessionId helper 2016-02-26 08:48:55 -08:00
Dan Sosedoff 86f63eecc5 Always include session id in get queries, fixes #142 2016-02-26 08:16:17 -08:00
Dan Sosedoff ff25927b52 Regenerate bindata 2016-02-24 20:23:51 -08:00
Dan Sosedoff 141e1a938d Merge pull request #141 from tylerlong/master
Table Information no vertical scroll bar
2016-02-24 20:22:57 -08:00
Tyler Long bab4f304c3 Table Information no vertical scroll bar
Fix https://github.com/sosedoff/pgweb/issues/140
2016-02-25 11:04:53 +08:00
Dan Sosedoff 5ecb66a4ef Fix connection button status while connection is being established, fixes #134 2016-02-20 21:03:35 -06:00
Dan Sosedoff 5ae5907d11 Merge pull request #135 from sosedoff/url-prefix
Add ability to launch pgweb with url prefix
2016-02-20 20:51:27 -06:00
Dan Sosedoff 24c5ecc437 Add options tests 2016-02-20 20:45:50 -06:00
Dan Sosedoff 0a144a633d Add ability to launch pgweb with url prefix 2016-02-19 21:14:56 -06:00
Dan Sosedoff f308d1cfde Fix issue with long table names in sidebar 2016-02-19 20:32:43 -06:00
Dan Sosedoff 3f4057b17c Merge pull request #133 from sosedoff/affected-rows
Return number of affected rows if sql query is update or delete
2016-02-18 22:47:48 -06:00
Dan Sosedoff 4b4f778360 Return number of affected rows if sql query is update or delete 2016-02-18 22:36:01 -06:00
Dan Sosedoff 6edc384c05 Only run actual query without any comments 2016-02-18 22:18:07 -06:00
Dan Sosedoff 540613645f Replace url-unsafe characters in base64 query data 2016-02-18 21:17:31 -06:00
Dan Sosedoff 586605557e Hide cancel button when disconnected 2016-02-05 17:44:36 -06:00
Dan Sosedoff f08c9d36a6 Add disconnect button to close active connection 2016-02-04 23:13:54 -06:00
Dan Sosedoff da43314332 Add disconnect endpoint 2016-02-04 23:05:42 -06:00
Dan Sosedoff ccd5455103 Add double quotes to column name in filters 2016-01-31 20:12:56 -06:00
Dan Sosedoff d3d408bc68 Version bump: 0.9.1 2016-01-25 19:16:21 -06:00
Dan Sosedoff a9bafda41c Fix bug with tables context menu 2016-01-22 13:36:06 -06:00
Dan Sosedoff 3d1fd4a7aa Bump ajax timeout to 5 mins 2016-01-22 12:14:19 -06:00
Dan Sosedoff 951bec8596 Go vet 2016-01-22 12:12:00 -06:00
Dan Sosedoff 5e47e8bd29 Setup ajax timeout to 2 minutes to allow long running queries 2016-01-21 20:13:12 -06:00
Dan Sosedoff d53c94eb91 Update readme 2016-01-21 20:01:53 -06:00
Dan Sosedoff 111d2cecf9 Reload sidebar if anything gets dropped/created 2016-01-20 21:33:36 -06:00
Dan Sosedoff eccb430a9b Automatically hide ssh fields if bookmark does not have ssh config 2016-01-20 20:10:17 -06:00
Dan Sosedoff bd207a3551 Fix js error when custom sql query returns no results 2016-01-20 09:21:31 -06:00
Dan Sosedoff d3f12802a3 Version bump: 0.9.0 2016-01-19 13:41:42 -06:00
Dan Sosedoff 218394fefc Hide ssh option when switching to url based connection option 2016-01-18 09:50:56 -06:00
Dan Sosedoff f036d41559 Merge pull request #114 from sosedoff/ssh-tunnel
SSH Tunnels
2016-01-17 15:38:49 -06:00
Dan Sosedoff ac5c18e261 Automatically switch to SSH panel if bookmark uses ssh 2016-01-17 15:28:22 -06:00
Dan Sosedoff e3f5cab6dd Add task to build on all PG versions 2016-01-17 15:22:51 -06:00
Dan Sosedoff 9bfaae7194 Merge with master 2016-01-17 15:22:33 -06:00
Dan Sosedoff 4844270f0b Return empty rows collection if query does not have any rows 2016-01-17 15:05:57 -06:00
Dan Sosedoff 3167d96cfc use a different query to fetch materialized view structure 2016-01-17 15:00:33 -06:00
Dan Sosedoff 84f1bd95e7 Use non-standart postgresql port for testing 2016-01-15 12:44:21 -06:00
Dan Sosedoff 9766bb2ed2 Add ability to run tests on postgresql 9.1-9.5 2016-01-15 12:33:30 -06:00
Dan Sosedoff 3af20f9327 Display materialized views 2016-01-14 21:30:19 -06:00
Dan Sosedoff 983f285373 Point db to local tunnel if ssh is enabled 2016-01-14 21:14:45 -06:00
Dan Sosedoff f0f447857f Tunnel implementation, allow using ssh on connection screen 2016-01-14 19:50:01 -06:00
Dan Sosedoff fb66acebc3 Use anonymous structs in the test 2016-01-14 16:55:05 -06:00
Dan Sosedoff 508e49e986 Fix issue with missing packages 2016-01-14 16:39:55 -06:00
Dan Sosedoff 44fe84a58a Update godeps, add crypto package 2016-01-14 16:33:43 -06:00
Dan Sosedoff 0bccd3a801 Update go version 2016-01-14 16:29:31 -06:00
Dan Sosedoff 2721a90455 Use session-storage to keep per-tab sessions instead of per-browser 2016-01-14 13:19:01 -06:00
Dan Sosedoff 52bd1ed1d4 Bindata 2016-01-13 01:29:31 -06:00
Dan Sosedoff 68c2b4d084 Initial ssh tunnel implementation 2016-01-13 01:29:14 -06:00
Dan Sosedoff ab94dd5b86 Merge pull request #112 from sosedoff/multiple-schemas
Multiple schemas
2016-01-12 22:10:41 -06:00
Dan Sosedoff 556e7d445a Remove unused sequences code 2016-01-12 22:05:32 -06:00
Dan Sosedoff fbaa61bbbc Regenerate bindata 2016-01-12 21:46:06 -06:00
Dan Sosedoff 8f4c5aab0f CSS tweaks 2016-01-12 21:41:58 -06:00
Dan Sosedoff a4084cb556 Switch travis to use postgres 9.4 2016-01-12 21:37:38 -06:00
Dan Sosedoff 9ffa05affb Initial support for multiple schemas 2016-01-12 21:33:44 -06:00
Dan Sosedoff 07d390b72e Changelog 2016-01-11 12:27:26 -06:00
Dan Sosedoff 5690518f25 Version bump: 0.8.0 2016-01-11 08:32:05 -06:00
Dan Sosedoff 79b2d46849 Update bindata 2016-01-11 08:31:45 -06:00
Dan Sosedoff 9c7eaf63d5 Bump go to 1.5.2 2016-01-10 17:41:14 -06:00
Dan Sosedoff 0ce796e1a4 Godeps go 1.5.1 2016-01-10 17:40:10 -06:00
Dan Sosedoff b5203b5c0d Update screenshots 2016-01-10 16:08:13 -06:00
Dan Sosedoff b14b1cd60d Change icon on sequences items on the sitebar 2016-01-10 15:50:16 -06:00
Dan Sosedoff 0c0d0ea3a3 Update readme 2016-01-10 15:41:07 -06:00
Dan Sosedoff d12b686e52 Merge pull request #111 from sosedoff/sessions
Database sessions
2016-01-10 15:36:02 -06:00
Dan Sosedoff 73185940b6 Allow enabling sessions via SESSIONS env var 2016-01-10 15:22:30 -06:00
Dan Sosedoff f3920afe8c Only expose sessions data in debug mode, otherwise return just a count of sessions 2016-01-10 15:16:31 -06:00
Dan Sosedoff ad1994574d Move api middleware into its own file 2016-01-10 15:03:33 -06:00
Dan Sosedoff d772ae0b31 Make sessions optional via --sessions CLI flag 2016-01-08 19:44:31 -06:00
Dan Sosedoff ed94244741 Make pgweb to use multiple sessions at once [wip] 2016-01-08 19:10:11 -06:00
Dan Sosedoff 73cb979721 Merge pull request #110 from sosedoff/filter-and-pagination
Filter and pagination
2016-01-08 15:44:41 -06:00
Dan Sosedoff b5d6368cca Do not show bindata.go in the git diff log 2016-01-08 15:34:02 -06:00
Dan Sosedoff be3b954716 Filtering bugfixes 2016-01-08 15:31:06 -06:00
Dan Sosedoff 1d05cfa0af Reset filters before loading table data 2016-01-08 15:06:33 -06:00
Dan Sosedoff 38c971ab49 Implement pagination and simple filtering 2016-01-08 14:16:53 -06:00
Dan Sosedoff 61523e33df Add support for offset in table rows endpoint, dry up code 2016-01-07 21:18:22 -06:00
Dan Sosedoff 9b5764d4fb Do not record duplicate queries into history 2016-01-07 20:55:23 -06:00
Dan Sosedoff 1bc824e39b Move query export buttons to the right, add xml export button 2016-01-07 20:44:03 -06:00
Dan Sosedoff e662e907be Merge pull request #109 from sosedoff/bigint-to-string
Convert bigints that exceed js number limits to strings
2016-01-07 20:20:01 -06:00
Dan Sosedoff 28178e46de Format big floats as well 2016-01-07 20:10:53 -06:00
Dan Sosedoff 3acc6febae Automatically call PrepareBigints for all queries 2016-01-07 11:33:08 -06:00
Dan Sosedoff fbb8ffbfc6 Encode bigints as strings 2016-01-07 11:27:16 -06:00
Dan Sosedoff 581a2f164b Update dockerfile to use golang 1.5 image 2016-01-05 14:11:45 -06:00
Dan Sosedoff 051d7d6eb4 Update changelog 2016-01-05 13:36:45 -06:00
Dan Sosedoff 7766c3bb66 Version bump: 0.7.0 2016-01-05 13:14:36 -06:00
Dan Sosedoff 35aa91de27 Readme, license 2016-01-05 00:41:50 -06:00
Dan Sosedoff 3a793ed87d More design tweaks 2016-01-05 00:31:14 -06:00
Dan Sosedoff 9cfdf4a194 Fresh design 2016-01-04 20:25:54 -06:00
Dan Sosedoff aa0ffedc8c Update sql editor theme to twilight, make font bigger 2016-01-04 19:17:51 -06:00
Dan Sosedoff 82c162d399 Fix appveyor builds 2016-01-04 18:51:25 -06:00
Dan Sosedoff 995d52db1b Merge pull request #107 from sosedoff/export
Export to JSON / XML
2016-01-04 18:40:43 -06:00
Dan Sosedoff c7e58f0828 Remove duplicate code 2016-01-04 18:35:05 -06:00
Dan Sosedoff 1f99576a37 Add test cases for result struct 2016-01-04 18:33:54 -06:00
Dan Sosedoff 91d8d3ee83 Move result struct into its own file 2016-01-04 18:19:16 -06:00
Dan Sosedoff 5a92c5508c Add ability to export query results to JSON 2016-01-04 18:15:05 -06:00
Dan Sosedoff 73a97893e9 Add ability to export table to JSON and XML 2016-01-04 18:03:53 -06:00
Dan Sosedoff bc1f876fb5 Merge pull request #104 from brianlow/constraints
Show table constraints
2015-12-08 18:33:51 -06:00
Brian Low eeb0fdf3a4 Show table constraints 2015-12-04 17:15:03 -07:00
Dan Sosedoff 4fb24cb229 Merge pull request #100 from niiyz/dev-sequence
Add sequence.
2015-11-16 21:19:57 -06:00
niiyz d35b3661a9 Make Assets. 2015-11-14 08:45:55 +09:00
niiyz 3e6e223ed0 Indent modification. 2015-11-13 22:43:19 +09:00
niiyz 8579edf1ca Indent modification. 2015-11-13 22:41:53 +09:00
niiyz 420d85e811 Indent modification. 2015-11-13 22:34:53 +09:00
niiyz 924a46894b Add Sequence to Front. app.js 2015-11-13 22:26:05 +09:00
niiyz 71aec1f07a Add Sequences to Front. 2015-11-13 21:38:33 +09:00
niiyz 4688e62b90 Add Sequences to API Test. 2015-11-13 21:36:51 +09:00
niiyz 232790b452 Add Sequences to API. 2015-11-13 21:36:13 +09:00
Dan Sosedoff 4c1bb758ce Update change log 2015-10-19 21:20:03 -05:00
Dan Sosedoff 6749b2ace8 Make sure to close fake server connection in tests 2015-10-19 21:09:22 -05:00
Dan Sosedoff 8f7fdcbdca Update ci config 2015-09-14 22:57:53 -05:00
Dan Sosedoff 1e50471a87 CI on 1.5 2015-08-20 01:25:01 -05:00
Dan Sosedoff 20a5728152 Version bump: 0.6.3 2015-08-16 10:24:21 -05:00
Dan Sosedoff 3170fb22cb Escape postgres password, GH-96 2015-08-15 21:11:09 -05:00
Dan Sosedoff c02d15ec34 Encode sql query to base64 for CSV export, GH-95 2015-08-15 20:53:51 -05:00
Dan Sosedoff 2a4edaf08f Add request middleware to log incoming form params 2015-08-04 10:38:23 -05:00
Dan Sosedoff 639ca96caa Write current sql query after input query change 2015-08-02 19:21:42 -05:00
Dan Sosedoff fb13ce8b07 Automatically save last executed query to localStorage 2015-07-30 22:29:08 -05:00
Dan Sosedoff 2aacab44ec Revert "Test on tip"
This reverts commit 279acc8af8.
2015-07-30 22:21:01 -05:00
Dan Sosedoff 279acc8af8 Test on tip 2015-07-29 21:58:16 -05:00
Dan Sosedoff 3765728342 Update changelog 2015-07-17 19:37:07 -05:00
Dan Sosedoff 68ceb212b5 Version bump: 0.6.2 2015-07-15 18:21:08 -05:00
Dan Sosedoff e52114e969 Update changelog 2015-07-14 23:05:36 -05:00
Dan Sosedoff e3b7dd013b Merge pull request #93 from sosedoff/travis-containers
Use container-based travis infrastructure
2015-07-14 22:58:54 -05:00
Dan Sosedoff cea3b6b43e Use container-based travis infrastructure 2015-07-14 22:56:40 -05:00
Dan Sosedoff 06d39e0ab7 Merge pull request #92 from sosedoff/postgresql-prefix
Support both postgresql:// and posgres:// url prefix
2015-07-14 22:54:58 -05:00
Dan Sosedoff 785108002e Support both postgresql:// and posgres:// url prefix 2015-07-14 22:42:46 -05:00
Dan Sosedoff c12853d0b0 Regenerate bindata 2015-07-14 22:27:48 -05:00
Dan Sosedoff f1afa0ac33 Merge pull request #87 from Supme/patch-1
Update index.html
2015-07-14 22:27:18 -05:00
Dan Sosedoff 5265016003 Merge pull request #91 from sosedoff/update-pq
Update pg dependency to latest
2015-07-14 22:27:11 -05:00
Dan Sosedoff c6cb3db624 Expose default port in dockerfile 2015-07-14 22:26:32 -05:00
Dan Sosedoff d35f58a2b5 Update pg dependency to latest 2015-07-14 22:11:02 -05:00
Dan Sosedoff d9ada2e58a Skip port tests on windows for now 2015-07-14 21:38:15 -05:00
Dan Sosedoff 4d6c688bf0 Merge pull request #90 from freeformz/patch-2
Some app.json updates to consider
2015-07-14 21:19:55 -05:00
Dan Sosedoff 3eec0947d2 Merge pull request #89 from freeformz/patch-1
Update app.json
2015-07-14 21:19:27 -05:00
Edward Muller 5979272115 Some app.json updates to consider
a) Consider using the secret generator so that deploys don't default to a known secret.
b) tag it with golang
2015-07-14 12:52:28 -07:00
Edward Muller 5fb70ace3c Update app.json
This is no longer needed as we now officially support Go: https://blog.heroku.com/archives/2015/7/7/go_support_now_official_on_heroku
2015-07-14 12:50:32 -07:00
Dan Sosedoff 5e4398ceae Update changelog 2015-06-18 13:15:35 -05:00
Dan Sosedoff 67a9820c98 Print SHA sum for binary when packaging 2015-06-18 13:14:54 -05:00
Dan Sosedoff 665f79d26c Version bump: 0.6.1 2015-06-18 13:10:02 -05:00
Alexey Agafonov 6152819e45 Update index.html 2015-06-10 17:06:28 +03:00
Dan Sosedoff 427a08029a Moar readme 2015-06-03 20:43:22 -05:00
Dan Sosedoff df7ef6d8f0 Tweak screens page 2015-06-03 20:34:45 -05:00
Dan Sosedoff b9b646bb11 Fix readme link 2015-06-03 20:33:01 -05:00
Dan Sosedoff ae62f8ea3d Add separate screens 2015-06-03 20:32:33 -05:00
Dan Sosedoff 0939632399 Update changelog 2015-05-31 20:48:06 -05:00
Dan Sosedoff 4b5a2b96a8 Print SHA256 of the zip archive, not the binary 2015-05-31 20:42:55 -05:00
Dan Sosedoff 79bf430dee Version bump: 0.6.0 2015-05-31 20:40:10 -05:00
Dan Sosedoff 62a168d44f Contributing guidelines 2015-05-20 21:50:22 -05:00
Dan Sosedoff 502c8dae91 Set test command for appveyor 2015-05-19 15:19:12 -05:00
Dan Sosedoff afdfc54802 Update screenshots 2015-05-19 12:57:24 -05:00
Dan Sosedoff 651b65a882 Implement table item context menu
- Add ability to export table contents into CSV
- Add ability to truncate table
- Add ability to delete table
2015-05-19 12:24:59 -05:00
Dan Sosedoff c2290acae3 Add port checking functionality and tests 2015-05-18 17:13:17 -05:00
Dan Sosedoff e1276afd71 Change sidebar color theme to light one 2015-05-18 16:57:53 -05:00
Dan Sosedoff dea0fb8b78 Tweak readme 2015-05-18 16:28:05 -05:00
Dan Sosedoff faebfb3e62 Regenerate assets 2015-05-18 16:22:19 -05:00
Dan Sosedoff 6fd7b7d427 Merge pull request #85 from brianlow/selected-text-1
Run selected text. Implements sosedoff/pgweb#78.
2015-05-18 16:20:38 -05:00
brianlow 385b415676 Run selected text. Implements sosedoff/pgweb#78.
In the Query tab, only run the selected text. If nothing selected, run all text in the editor.
2015-05-18 14:58:03 -06:00
Dan Sosedoff 50f0f5f6dc Shields 2015-05-09 22:12:38 -05:00
Dan Sosedoff 8889847c71 Fix version 2015-05-08 09:00:24 -05:00
Dan Sosedoff d26bb75ebc Tweak packaging script 2015-05-06 21:31:05 -05:00
Dan Sosedoff a65cca44e5 Add script to package binaries 2015-05-06 21:24:15 -05:00
Dan Sosedoff 18b0260d1f Changelog 2015-05-06 21:03:00 -05:00
Dan Sosedoff c9d4ef5e89 Tweak table row styles 2015-05-06 20:58:21 -05:00
Dan Sosedoff c0f3f027ae Move version definition into command package, include build timestamp, add /api/info endpoint 2015-05-05 00:34:23 -05:00
Dan Sosedoff 0bd5360315 Update styles for connection window 2015-05-05 00:15:30 -05:00
Dan Sosedoff 29b46ba59c Update bindata 2015-05-02 20:50:01 -05:00
Dan Sosedoff 04fe0023b7 DRY up api module 2015-05-02 20:32:16 -05:00
Dan Sosedoff cb3c3e0e2e Fix function names in api module 2015-05-02 20:13:04 -05:00
Dan Sosedoff 0ac9d72deb Refactor and simplify asset serving 2015-05-02 20:10:14 -05:00
Dan Sosedoff dd2200bdb8 Tweak appveyor config 2015-04-30 21:24:53 -05:00
Dan Sosedoff ca894c621f Inject git version when building a release 2015-04-30 20:40:06 -05:00
Dan Sosedoff 4224b3d326 Fix docker build and specify bind interface for command 2015-04-30 20:22:08 -05:00
Dan Sosedoff 891f45c1ca Split up api into smaller files 2015-04-30 19:59:48 -05:00
Dan Sosedoff 595f51d76c Add test task to makefile readme 2015-04-30 12:10:25 -05:00
Dan Sosedoff 50cdf99913 Fix all tests 2015-04-30 12:09:29 -05:00
Dan Sosedoff e9f2f8eb83 Test all 2015-04-30 11:53:21 -05:00
Dan Sosedoff 73d2aa8fe1 Check in compiled assets 2015-04-30 11:51:17 -05:00
Dan Sosedoff 579eaf9141 Do not uri encode password for now 2015-04-30 11:51:02 -05:00
Dan Sosedoff c513930e27 Restructure application 2015-04-30 11:47:07 -05:00
Dan Sosedoff 7a75447364 Encode connection password so it could be passed via get request 2015-04-29 20:03:39 -05:00
Dan Sosedoff ec6bb5590d Tweak js formatting, highlight header with sort order 2015-04-28 10:01:34 -05:00
Dan Sosedoff 637e7a4e24 Change default http port to 8081 to not conflict with rethinkdb web ui 2015-04-28 09:49:36 -05:00
Dan Sosedoff 08a70baf3f Remove go tip from travis config for now 2015-04-20 21:30:55 -05:00
Dan Sosedoff bc43ca6f0f Changelog 2015-04-13 09:03:52 -05:00
Dan Sosedoff 65ba084d80 Version bump: 0.5.2 2015-04-13 09:03:30 -05:00
Dan Sosedoff 961ad31c13 Changelog 2015-04-09 08:46:18 -05:00
Dan Sosedoff 40509cc5fd Rebuild bindata.go 2015-04-09 08:36:32 -05:00
Dan Sosedoff 7606a88293 Merge pull request #83 from benjamin-thomas/sort-columns
Sort columns
2015-04-09 08:34:31 -05:00
Benjamin Thomas d3beb57f15 Update bindata 2015-04-09 09:00:58 +02:00
Benjamin Thomas 691106326f Add sort functionality, within content tab 2015-04-09 08:59:10 +02:00
Benjamin Thomas c82205ce4f Remove trailing whitespace 2015-04-09 08:49:17 +02:00
Dan Sosedoff f0552ab72d Add /api/schemas endpoint to list all database schemas 2015-03-30 23:58:04 -05:00
Dan Sosedoff 786081aafa Changelog 2015-03-21 19:51:33 -05:00
Dan Sosedoff 0251a8bb73 Merge pull request #82 from sosedoff/query-activity
Query activity
2015-03-21 19:48:47 -05:00
Dan Sosedoff 2a78500fdf Add activity tab 2015-03-21 19:14:11 -05:00
Dan Sosedoff f68d81ff17 Update assets 2015-03-21 11:46:32 -05:00
Dan Sosedoff 38ecb5ffc4 Add new endpoint /activity that returns active queries 2015-03-21 11:46:14 -05:00
Dan Sosedoff fa8f3a2997 Add docker-release to usage 2015-03-17 00:01:28 -05:00
Dan Sosedoff 00f4413f12 Add docker-release target to makefile 2015-03-16 23:59:46 -05:00
Dan Sosedoff d4d97edeef Update dockerfile base to golang:1.4.2 2015-03-16 23:51:24 -05:00
Dan Sosedoff 14cc8d5c24 Move all test data under ./data dir 2015-03-16 23:41:53 -05:00
Dan Sosedoff cc983f9ad6 Hide sql query when switching to table structure view 2015-03-09 14:03:54 -05:00
Dan Sosedoff 0764b9263d Run travis on 1.4.1/1.4.2 2015-03-04 09:06:48 -06:00
Dan Sosedoff a207b66b7e Fix bookmarks tests 2015-03-04 08:58:53 -06:00
Dan Sosedoff d42bd2e803 Add sample coverage file to gitignore 2015-03-04 08:53:41 -06:00
Dan Sosedoff 27b3cac1e0 Run coverage analysis with tests 2015-03-04 08:53:16 -06:00
Dan Sosedoff 866cdccd49 Make bookmark tests platform-independent 2015-03-03 21:01:20 -06:00
Dan Sosedoff 1e58c94598 Add bookmark tests 2015-03-03 20:44:14 -06:00
Dan Sosedoff 96bcaa5baa Reduce amount of error printing when reading bookmarks 2015-03-03 20:14:18 -06:00
Dan Sosedoff fe9d7e459d Update changelog 2015-02-23 20:17:20 -06:00
329 changed files with 11812 additions and 36231 deletions
+3
View File
@@ -0,0 +1,3 @@
.github
bin/
./pgweb
+1
View File
@@ -0,0 +1 @@
bindata.go -diff
+113
View File
@@ -0,0 +1,113 @@
name: checks
on:
push:
branches:
- main
paths-ignore:
- '**.md'
pull_request:
types:
- opened
- synchronize
paths-ignore:
- '**.md'
env:
GO_VERSION: "1.25"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
tests:
name: tests
runs-on: ubuntu-latest
timeout-minutes: 40
strategy:
matrix:
pg_version: [9.6, 10, 11, 12, 13, 14, 15, 16, 17, 18]
services:
postgres:
image: postgres:${{ matrix.pg_version }}
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: booktown
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Install latest Postgres client
run: |
sudo rm -f /etc/apt/sources.list.d/pgdg.list
curl --silent https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add
echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list
sudo apt-get update && sudo apt-get install -y postgresql-client-18
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- run: go mod download
- run: make test
env:
MallocNanoZone: 0 # https://github.com/golang/go/issues/49138
PGHOST: localhost
PGUSER: postgres
PGPASSWORD: postgres
PGDATABASE: booktown
tests-windows:
runs-on: windows-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- run: go mod download
- run: make test
lint:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: v2.7.2
fmt:
name: fmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- run: go mod download
- run: script/check_formatting.sh
+20
View File
@@ -0,0 +1,20 @@
name: demo deploy
on:
push:
branches:
- main
env:
FLY_API_TOKEN: ${{ secrets.FLY_TOKEN }}
jobs:
deploy:
name: Deploy to Fly
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
+37
View File
@@ -0,0 +1,37 @@
name: docker
on:
push:
branches:
- main
env:
GO_VERSION: "1.25"
CGO_ENABLED: 0
IMAGE_REPOSITORY: sosedoff/pgweb
jobs:
docker-build:
name: docker images
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Configure docker build context
uses: docker/setup-buildx-action@v2
- name: Build docker images
uses: docker/build-push-action@v2
with:
context: .
push: false
tags: pgweb:latest
platforms: linux/amd64,linux/arm64,linux/arm/v7
build-args: |
"CGO_ENABLED=${{ env.CGO_ENABLED }}"
+60
View File
@@ -0,0 +1,60 @@
name: release
on:
push:
tags:
- "v*"
env:
GO_VERSION: "1.25"
CGO_ENABLED: 0
DOCKER_REPOSITORY: sosedoff/pgweb
GHCR_REPOSITORY: sosedoff/pgweb
jobs:
docker-release:
name: Publish Docker images
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Configure docker build context
uses: docker/setup-buildx-action@v2
- name: Set reference tags
id: refs
run: |
echo ::set-output name=SOURCE_NAME::${GITHUB_REF#refs/*/}
echo ::set-output name=SOURCE_BRANCH::${GITHUB_REF#refs/heads/}
echo ::set-output name=SOURCE_TAG::${GITHUB_REF#refs/tags/v}
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to Github Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GH_TOKEN }}
- name: Build and push docker images
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: |
${{ env.DOCKER_REPOSITORY }}:${{ steps.refs.outputs.SOURCE_TAG }}
${{ env.DOCKER_REPOSITORY }}:latest
ghcr.io/${{ env.GHCR_REPOSITORY }}:${{ steps.refs.outputs.SOURCE_TAG }}
ghcr.io/${{ env.GHCR_REPOSITORY }}:latest
platforms: linux/amd64,linux/arm64,linux/arm/v7
+4
View File
@@ -1,4 +1,8 @@
.DS_Store
.idea
.env
.envrc
pgweb
bin
tmp/
cover.out
+7
View File
@@ -0,0 +1,7 @@
version: "2"
linters:
disable:
- errcheck
settings:
staticcheck:
checks: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022", "-ST1005", "-QF1004"]
-16
View File
@@ -1,16 +0,0 @@
language: go
addons:
postgresql: "9.3"
go:
- 1.3
- 1.4
- tip
install:
- make setup
script:
- make build
- make test
+407 -1
View File
@@ -1,4 +1,410 @@
## 0.5.1 - Unreleased
## Changelog
Current [release](https://github.com/sosedoff/pgweb/releases) is `0.17.0`.
## 0.17.0 - 2025-11-22
- `NEW` Update Dockerfile to use Golang 1.24 image, GH-821
- `NEW` Bump go crypto package to 0.44.x, GH-820, GH-823
- `NEW` Add PostgreSQL 18 support to Github Actions, GH-816
- `NEW` Connect backend refactor, GH-801
- `NEW` Add server settings view, GH-768
- `NEW` Add exec time of empty queries, GH-763
- `FIX` Minor typos, GH-764, GH-786
- `FIX` Shorten git revision printed in the --version output, GH-770
## 0.16.2 - 2024-11-02
- `FIX` Build a new Docker image with PostgreSQL 17 support
- `FIX` Run CI against PostgreSQL 17, GH-758
- `FIX` Rename master to main branch, GH-750
## 0.16.1 - 2024-09-07
- `FIX` Remove linux/arm/v5 from docker release action, GH-742
## 0.16.0 - 2024-06-04
- `NEW` Allow database stats downloads, GH-738
- `NEW` Add analyze table action, GH-737
- `NEW` Bump postgres version used in docker compose to 15, GH-729
- `NEW` Build on Go 1.22, GH-726
- `FIX` SSH tunnel cleanup and parse fixup, GH-731
- `FIX` Drop linux/arm/v5 from docker build, GH-728
- `FIX` Propagate CGO_ENABLED environment variable to docker build, GH-724
## 0.15.0 - 2024-03-14
- `NEW` Add support for a bookmarks-only mode, GH-716
- `FIX` Fix missing indexes by quoting schema/table name to ::regclass, GH-711
- `FIX` Continue on parseJSON error, GH-708
## 0.14.3 - 2024-01-28
- `NEW` Allow retrying a connection on startup, GH-695
- `NEW` Allow setting readonly mode in bookmarks, GH-707
- `FIX` Add UPDATE to list of restricted keywords in read-only mode, GH-697
## 0.14.2 - 2023-10-29
- `NEW` Execute tests using PostgreSQL 16, GH-691
- `FIX` Unclosed database sessions and tunnels, GH-688
- `FIX` Use pg_table_size for table stats query, GH-685
- `FIX` Use `HasSuffix` to correctly determine URL prefix, GH-684
## 0.14.1 - 2023-06-17
- `NEW` Add process start time metric, GH-675
- `NEW` Configure pgweb user for docker container, GH-674
- `NEW` Updated dockerfile, GH-645
- `FIX` Fix typo in the healthy metric, GH-657
- `NEW` Use entrypoint instead of cmd in in dockerfile, GH-654
## 0.14.0 - 2023-02-21
- `FIX` History page query loading fixup, GH-632
- `NEW` Display cell content via context menu, GH-634
- `NEW` Handle support/permissions errors in info call, GH-635
- `NEW` Show error message when API calls fail, GH-636
- `NEW` Add bookmark options to load username/password from env vars, GH-638
- `NEW` Add context menu to display database tables stats, GH-639
- `NEW` Added Local Queries feature, GH-641
- `FIX` Ensure that objects are sorted by schema and name, GH-648
- `FIX` Fetch local queries on db connect, GH-650
## 0.13.1 - 2022-12-27
- Fix connect flow when `~/.pgweb/bookmarks` directory is not available, GH-631
## 0.13.0 - 2022-12-25
- Add support for .pgpass file, GH-617
- Request logging additions (request id, forwarded user), GH-618
- Establish connections using bookmark ID only, GH-619
- Display empty schemas on the sidebar, GH-621
- Configure timeout and retries when testing connection status, GH-623
- Setup basic prom metrics endpoint, GH-624
- Add default connect_timeout option to connection string, GH-626
- Add duration_ms to log entries, GH-628
- Add query execution stats to api endpoint, GH-629
## 0.12.0 - 2022-12-13
- Deprecate usage of Gox for binary builds, GH-571
- Add netcat install in dockerfile to provide a way to healthcheck, GH-572
- Install latest postgres client in docker image, GH-577
- Add support for `PGWEB_` prefix environment variables, GH-585
- Fix export URL generation, refactor export code, GH-588
- Add logrus-based request logger, GH-589
- Configure logger for connect backend, GH-591
- Set LDFLAGS for make build/release commands, GH-592
- Add internal sessions manager, GH-593
- Include index size on the index list view, GH-595
- Fix flaky backend connection test, GH-596
- Add ability to view and copy views/materialized views definitions, GH-594
- Enable dev assets mode with PGWEB_ASSETS_DEVMODE env var, GH-597
- Make query input box resizable, GH-599
- Deprecate Heroku demo deployments and switch to Fly, GH-600
- Handle returning values in update/delete queries, GH-601
- Fix panic with invalid time marshaling, GH-602
- Configure logging level and format, GH-605
- Use go embed to load queries from static files, GH-607
- Switch go build target to 1.19, GH-603
- Add support for user functions, GH-608
- Implement global query timeout option, GH-609
- Switch windows tests from Appveyor to Github Actions, GH-611
- Fix activity endpoint panic when server version is not detected, GH-612
## 0.11.12 - 2022-07-05
- Update base docker image (alpine), update deps, GH-558
- Refactor docker images building, include ARM, GH-568
## 0.11.11 - 2022-03-29
- Auto-detect the query from the query source based on user selection, GH-547
- Added binary codec base58 as well as improving the help for --binary-codec flag, GH-548
- Change binary codec back to none, GH-555
## 0.11.10 - 2022-01-20
- Removes alert on column copy value, GH-536
- Migrate test suite to Github Action, GH-540
- Serialize binary bytea cols into hex/base64, GH-537
- Include build time into version string, GH-541
- Explain analyze dropdown button, GH-532
- Switch to go 1.17, GH-543
- Use HTTP 302 status code for successful backend redirect, GH-544
- Add connect backend tests, GH-546
## 0.11.9 - 2021-11-08
- Releases are built on Go 1.17
- Build time correction, GH-521
- Fix broken assets URL path prefix, GH-525
- Update docker build image to alpine:3.14, GH-522
- Upgrade gin dependency to v1.7.4, GH-527
- Add FreeBSD startup script, GH-520
## 0.11.8 - 2021-07-07
- Releases are built with Go 1.16
- Add ARM64 v7 build target, GH-497
- Switch to Go modules for dependency management, GH-509
- Switch to Go embed for static assets management, GH-510
- Add Darwin/ARM64 build target (Apple Silicon), GH-513
## 0.11.7 - 2020-10-18
- Releases are built with Go 1.15
- Show results row context menu on custom query results, GH-457
- Do not terminate if local authentication failed on start, GH-463
- Do not show other databases if session is locked, GH-470
- Strip debug information from binary to reduce size, GH-489
- Disable autocomplete on database search field, GH-492
- Improve windows connection error matching during start, GH-493
## 0.11.6 - 2020-02-19
- Add CLI options for SSL key, cert and root certs, GH-452
- Remove double click action on cell, GH-455
## 0.11.5 - 2019-12-16
- Add basic SQL keyword autocompletion, GH-443
- SSH Private Key handling update (encrypted keys are supported now), GH-445
- Include Go version into `pgweb --version` output, GH-447
- Fix long table name bug in the sidebar, GH-448
- Add SQL objects (table,views,etc) autocompletion, GH-449
- Include Go version into info API endpoint, GH-450
## 0.11.4 - 2019-10-05
- Fix SQL export filename, GH-438
- Update Docker image to alpine:3.10, GH-439
- Drop unsupported pg_dump options from connection string, GH-441
- Misc code cleanup and formatting, GH-442
## 0.11.3 - 2019-07-24
- Misc: add script to update homebrew formula version, GH-423
- Destructive keyword restriction in read-only mode, GH-421
- Make database object searchable in sidebar, GH-434
- Update lib/pg to 1.1.1, GH-435
## 0.11.2 - 2019-02-15
- Fix table row estimation query for camelcase schemas, GH-414
## 0.11.1 - 2019-01-28
- Typo fixes
- Add Base64 javascript encoder/decoder to replace deprecated window.atob call, GH-405
- Fix startup error when DATABASE_URL is set, GH-406
- Fix user auto detection when USER env var is not set, GH-408
- Switch bindata dependency to use maintained fork: github.com/go-bindata/go-bindata, GH-409
## 0.11.0 - 2018-12-24
- Tweak sidebar database object counters styles, GH-400
- Do not exit with error if local server is not running, GH-399
- Fix SSH host verification check, GH-398
- Scope activity list to current database only, GH-397
- Show current release version and check for updates, GH-396
- Force switch back to default connection settings view, GH-395
- Fix row count estimation bug, GH-394
- Print out failed query SQL and args with --debug flag, GH-393
## 0.10.0 - 2018-11-28
- Fixes relation not found errors when dealing with table names that have uppercase characters, GH-356
- Dockerfile updates, GH-357
- Check if pg_dump is available before running database export, GH-358
- Improvements to CockroachDB integration, GH-365
- Add EstimatedTableRowsCount to avoid count in large tables, GH-366
- Automatically set table filter option to 'equals' if its not set, GH-370
- Dependencies update and switch to dep, GH-375
- Add column context menu item to get numeric stats, GH-377
- Fix issues with connection string builder, GH-378
- Include rows count to numeric stats view on table column, GH-379
- Make localhost to be a default db host, GH-380
- Clear out connection settings/bookmark on login screen when running in session/connect mode
- Add table row context menu with actions, GH-381
- Allow settings url prefix with URL_PREFIX env var, GH-387
- Fix JSON marshal panic when dealing with NaN values, GH-388
- Fix startup behavior when user did not provide a database name, GH-389
## 0.9.12 - 2018-04-23
- Add link to view database connection string format on login page
- Include constraint name under "constraints" tab, GH-343
- Misc CI and config changes
## 0.9.11 - 2017-12-07
- Fix ssl mode for the connection url in the bookmarks, GH-320
- Add support for CORS, GH-321
- Fix custom query results counter for empty queries, GH-322
- Reorganize the table context menu, GH-323
- Disable database connection string text field autocomplete, GH-327
- Add db prefix to the table export files, GH-329
- Add database view context menu with export actions, GH-330
## 0.9.10 - 2017-11-03
- Make idle connection timeout configurable, [GH-282]
- Fix panics when sshinfo is not set on bookmarks, [GH-296]
- Dot now allow using startup bookmark in multi-session mode, [GH-300]
- Add ability to copy table name from the sidebar, [GH-301]
## 0.9.9 - 2017-09-28
- Automatically format JSON data exports, GH-255
- Update Docker image to alpine:3.6, GH-256
- Print out PostgreSQL server version on start in a single-session mode, GH-264
- Record last query timestamp for the client connection, GH-265
- Add context menu for table headers in browse mode (copy name, see unique values), GH-268
- Add ability to export current database dump, GH-270
- Automatically open pgweb in browser on start if its already running, GH-272
- Connect to the database with credentials provided by a third-party backend, GH-266
- Automatically close idle sessions (no activity in 1 hour), GH-275
- Allow connecting via SSH with a custom private key and other fixes, GH-277
- Add options to disable SSH connections, GH-279
## 0.9.8 - 2017-08-04
- Fixed error checking in the API, GH-234
- Fixed activity tab to support PG 9.x versions, GH-237
- Remember sort column and order for pagination, GH-240
- Use `sslmode=disable` for bookmarks without sslmode option, GH-244
- Javascript fixes for IE9-11, GH-245
- Require confirmation for the disconnect, GH-246
- Clean the results table on manual disconnect
## 0.9.7 - 2017-04-04
- Fixed issue with locked session and empty db url, GH-206
- Fixed path rewrite on DB change, GH-212
- Upgraded dependencies, GH-217
- Added ability to specify bookmarks path, GH-218
- Added counter for the number of rows from a custom SQL query, GH-224
- Added new behavior for removing table rows view on custom SQL query page, GH-225
## 0.9.6 - 2016-11-18
- Fixed bug in query base64-encoding, GH-186
- Fixed rows pagination visibility bug, GH-190
- Fixed issue with query order escaping, GH-191
- Fixed invalid query selection for explain command, GH-198
- Fixed issue with empty sidebar, now it shows empty state, GH-202
- Added new flag --readonly to enable read only transaction mode, GH-193
- Added ability to kill any running query, GH-194
- Added session database connection locking, GH-195
- Added ability to switch between databases, GH-196
- Added feature to keep last selected tab when switching between tables, GH-197
- Added new flag --bookmark (-b) to specify server connection from bookmark, GH-201
## 0.9.5 - 2016-10-01
- Only view schema with USAGE privileges, GH-167
- Fixed broken export to CSV/JSON/XML if hashmark in URL, GH-175
- Added example service configuration for systemd, GH-177
- Allow setting auth user and pass using variables
## 0.9.4 - 2016-07-29
- Fixes CSV/JSON/XML export buttons when pgweb is running with url prefix, GH-170
## 0.9.3 - 2016-06-30
- Uses Go 1.6 for development, GH-155
- Fixes timestamp formatting in CSV export, GH-163
- Included PostgreSQL 9.6 for integration testing
- Switches docker image to Alpine to reduce image size
- Adds support for ARMv5
## 0.9.2 - 2016-03-01
- Fixes bug with unsafe base64 encoded sql queries
- Fixes issue with session id not being included in multi-session mode
- Fixes visual issue with long table names in sidebar
- Fixes visual issue with a scrollbar in table information widget
- Fixes issue with database connection form being reset by clicking on 'cancel' button
- Adds ability to close connection
- Adds display message for number of affected rows for update/delete queries, GH-133
- Adds web server url prefix as a CLI option, GH-135
## 0.9.1 - 2016-01-25
- Fixes bug with tables context menu
- Fixes JS bug when query returns no rows
- Fixes bug with switching between different connection modes
- Adds AJAX timeout to 5s
- Adds sidebar reload action on any CREATE/DROP action
## 0.9.0 - 2016-01-19
- Add support for multiple schemas. GH-112
- Add support for native ssh tunnes. GH-114
- Add materialized views to list of schema objects
- Adds a few design tweaks and cleanups
- Fixes bug with nil result set when fetching rows
## 0.8.0 - 2016-01-11
- Fixes bug with bigint conversions in javascript. Now bigints are encoded as strings. GH-109
- Adds pagination and simple column filtering to table rows browser. GH-110
- Adds ability to use pgweb with multiple database sessions. GH-111
- Adds a few design tweaks and cleanups
## 0.7.0 - 2016-01-05
- Adds sequences to the sidebar panel - GH-100
- Adds table constrains view - GH-104
- Adds ability to export table and query rows as JSON/XML - GH-107
- Updates to UI theme and SQL editor
## 0.6.3 - 2015-08-16
- Adds PostgreSQL password escaping in web ui, GH-96
- Adds base64 query encoding for CSV export, GH-95
- Adds automatic saving of last executed query to localStorage
- Adds request middleware to log incoming form params in debug mode
## 0.6.2 - 2015-07-15
- Adds ability to specify connection strings prefixed by `postgresql://`, [GH-92]
- Updates configuration for Heroku, [GH-89], [GH-90]
- Updates postgresql library dependency to latest, [GH-91]
- Fixes password field to not display plaintext passwords, [GH-87]
## 0.6.1 - 2015-06-18
- This release is repackage-release targeted to fix binary downloads
## 0.6.0 - 2015-05-31
- Adds ability to execute only selected SQL query in run command view, [GH-85]
- Adds ability to delete/truncate table via context many on sidebar view
- Adds ability to export table contents to CSV via context menu on sidebar view
- Changes sidebar color scheme to a lighter and better looking one
## 0.5.3 - 2015-05-06
- Changes default server port from 8080 to 8081 to avoil conflict with RethinkDB
- Changes styles for table rows and connection settings window
- Adds highlighting styles for columns with sort order
- Adds git sha into program version output
- Add new endpoint /api/info to get build details
## 0.5.2 - 2015-04-13
- Adds a new endpoint /activity that returns active queries
- Adds tab to view active queries
- Adds column sorting when browsing table contents
- Fixes SQL query view when switching to table structure view
## 0.5.1 - 2015-02-23
- Upgrades Gin framework dependency to 0.5.0
- Fixes server crash if another pgweb server is running
+7
View File
@@ -0,0 +1,7 @@
- Fork repository
- Create a new git branch
- Make changes
- Run tests: `make test`
- Run tests against all supported PostreSQL versions: `make test-all` (optional)
- If you change frontend code (js/css) make sure to rebuild assets: `make assets`
- Open a new pull request
+45 -7
View File
@@ -1,10 +1,48 @@
FROM golang:1.4.1
# ------------------------------------------------------------------------------
# Builder Stage
# ------------------------------------------------------------------------------
FROM golang:1.25-trixie AS build
COPY . /go/src/pgweb
WORKDIR /go/src/pgweb
# Set default build argument for CGO_ENABLED
ARG CGO_ENABLED=0
ENV CGO_ENABLED=${CGO_ENABLED}
RUN go get github.com/tools/godep
RUN godep restore
RUN godep go build && godep go install
WORKDIR /build
CMD ["pgweb"]
RUN git config --global --add safe.directory /build
COPY go.mod go.sum ./
RUN go mod download
COPY Makefile main.go ./
COPY static/ static/
COPY pkg/ pkg/
COPY .git/ .
RUN make build
# ------------------------------------------------------------------------------
# Fetch signing key
# ------------------------------------------------------------------------------
FROM debian:trixie-slim AS keyring
ADD https://www.postgresql.org/media/keys/ACCC4CF8.asc keyring.asc
RUN apt-get update && \
apt-get install -qq --no-install-recommends gpg
RUN gpg -o keyring.pgp --dearmor keyring.asc
# ------------------------------------------------------------------------------
# Release Stage
# ------------------------------------------------------------------------------
FROM debian:trixie-slim
ARG keyring=/usr/share/keyrings/postgresql-archive-keyring.pgp
COPY --from=keyring /keyring.pgp $keyring
RUN . /etc/os-release && \
echo "deb [signed-by=${keyring}] http://apt.postgresql.org/pub/repos/apt/ ${VERSION_CODENAME}-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
apt-get update && \
apt-get install -qq --no-install-recommends ca-certificates openssl netcat-openbsd curl postgresql-client
COPY --from=build /build/pgweb /usr/bin/pgweb
RUN useradd --uid 1000 --no-create-home --shell /bin/false pgweb
USER pgweb
EXPOSE 8081
ENTRYPOINT ["/usr/bin/pgweb", "--bind=0.0.0.0", "--listen=8081"]
-43
View File
@@ -1,43 +0,0 @@
{
"ImportPath": "github.com/sosedoff/pgweb",
"GoVersion": "go1.4",
"Deps": [
{
"ImportPath": "github.com/BurntSushi/toml",
"Comment": "v0.1.0-9-g3883ac1",
"Rev": "3883ac1ce943878302255f538fce319d23226223"
},
{
"ImportPath": "github.com/gin-gonic/gin",
"Comment": "v0.5",
"Rev": "50e7e427300a7eeeb299a4cd5bc1ebfa04d48528"
},
{
"ImportPath": "github.com/jessevdk/go-flags",
"Comment": "v1-285-g1679536",
"Rev": "1679536dcc895411a9f5848d9a0250be7856448c"
},
{
"ImportPath": "github.com/jmoiron/sqlx",
"Comment": "sqlx-v1.0-61-gb468c08",
"Rev": "b468c08552f4efac78b94708eb040170a8184c47"
},
{
"ImportPath": "github.com/julienschmidt/httprouter",
"Rev": "b55664b9e92004aebb7f19a19a9d06271f3a41fc"
},
{
"ImportPath": "github.com/lib/pq",
"Comment": "go1.0-cutoff-13-g19eeca3",
"Rev": "19eeca3e30d2577b1761db471ec130810e67f532"
},
{
"ImportPath": "github.com/mitchellh/go-homedir",
"Rev": "7d2d8c8a4e078ce3c58736ab521a40b37a504c52"
},
{
"ImportPath": "github.com/stretchr/testify/assert",
"Rev": "2eaa4b48b8954bf871229043b8064ca156a542a5"
}
]
}
Generated
-5
View File
@@ -1,5 +0,0 @@
This directory tree is generated automatically by godep.
Please do not edit.
See https://github.com/tools/godep for more information.
-2
View File
@@ -1,2 +0,0 @@
/pkg
/bin
@@ -1,5 +0,0 @@
TAGS
tags
.*.swp
tomlcheck/tomlcheck
toml.test
-12
View File
@@ -1,12 +0,0 @@
language: go
go:
- 1.1
- 1.2
- tip
install:
- go install ./...
- go get github.com/BurntSushi/toml-test
script:
- export PATH="$PATH:$HOME/gopath/bin"
- make test
-3
View File
@@ -1,3 +0,0 @@
Compatible with TOML version
[v0.2.0](https://github.com/mojombo/toml/blob/master/versions/toml-v0.2.0.md)
-14
View File
@@ -1,14 +0,0 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
-19
View File
@@ -1,19 +0,0 @@
install:
go install ./...
test: install
go test -v
toml-test toml-test-decoder
toml-test -encoder toml-test-encoder
fmt:
gofmt -w *.go */*.go
colcheck *.go */*.go
tags:
find ./ -name '*.go' -print0 | xargs -0 gotags > TAGS
push:
git push origin master
git push github master
-220
View File
@@ -1,220 +0,0 @@
## TOML parser and encoder for Go with reflection
TOML stands for Tom's Obvious, Minimal Language. This Go package provides a
reflection interface similar to Go's standard library `json` and `xml`
packages. This package also supports the `encoding.TextUnmarshaler` and
`encoding.TextMarshaler` interfaces so that you can define custom data
representations. (There is an example of this below.)
Spec: https://github.com/mojombo/toml
Compatible with TOML version
[v0.2.0](https://github.com/mojombo/toml/blob/master/versions/toml-v0.2.0.md)
Documentation: http://godoc.org/github.com/BurntSushi/toml
Installation:
```bash
go get github.com/BurntSushi/toml
```
Try the toml validator:
```bash
go get github.com/BurntSushi/toml/cmd/tomlv
tomlv some-toml-file.toml
```
[![Build status](https://api.travis-ci.org/BurntSushi/toml.png)](https://travis-ci.org/BurntSushi/toml)
### Testing
This package passes all tests in
[toml-test](https://github.com/BurntSushi/toml-test) for both the decoder
and the encoder.
### Examples
This package works similarly to how the Go standard library handles `XML`
and `JSON`. Namely, data is loaded into Go values via reflection.
For the simplest example, consider some TOML file as just a list of keys
and values:
```toml
Age = 25
Cats = [ "Cauchy", "Plato" ]
Pi = 3.14
Perfection = [ 6, 28, 496, 8128 ]
DOB = 1987-07-05T05:45:00Z
```
Which could be defined in Go as:
```go
type Config struct {
Age int
Cats []string
Pi float64
Perfection []int
DOB time.Time // requires `import time`
}
```
And then decoded with:
```go
var conf Config
if _, err := toml.Decode(tomlData, &conf); err != nil {
// handle error
}
```
You can also use struct tags if your struct field name doesn't map to a TOML
key value directly:
```toml
some_key_NAME = "wat"
```
```go
type TOML struct {
ObscureKey string `toml:"some_key_NAME"`
}
```
### Using the `encoding.TextUnmarshaler` interface
Here's an example that automatically parses duration strings into
`time.Duration` values:
```toml
[[song]]
name = "Thunder Road"
duration = "4m49s"
[[song]]
name = "Stairway to Heaven"
duration = "8m03s"
```
Which can be decoded with:
```go
type song struct {
Name string
Duration duration
}
type songs struct {
Song []song
}
var favorites songs
if _, err := toml.Decode(blob, &favorites); err != nil {
log.Fatal(err)
}
for _, s := range favorites.Song {
fmt.Printf("%s (%s)\n", s.Name, s.Duration)
}
```
And you'll also need a `duration` type that satisfies the
`encoding.TextUnmarshaler` interface:
```go
type duration struct {
time.Duration
}
func (d *duration) UnmarshalText(text []byte) error {
var err error
d.Duration, err = time.ParseDuration(string(text))
return err
}
```
### More complex usage
Here's an example of how to load the example from the official spec page:
```toml
# 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
# Line breaks are OK when inside arrays
hosts = [
"alpha",
"omega"
]
```
And the corresponding Go types are:
```go
type tomlConfig struct {
Title string
Owner ownerInfo
DB database `toml:"database"`
Servers map[string]server
Clients clients
}
type ownerInfo struct {
Name string
Org string `toml:"organization"`
Bio string
DOB time.Time
}
type database struct {
Server string
Ports []int
ConnMax int `toml:"connection_max"`
Enabled bool
}
type server struct {
IP string
DC string
}
type clients struct {
Data [][]interface{}
Hosts []string
}
```
Note that a case insensitive match will be tried if an exact match can't be
found.
A working example of the above can be found in `_examples/example.{go,toml}`.
@@ -1,14 +0,0 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
@@ -1,14 +0,0 @@
# Implements the TOML test suite interface
This is an implementation of the interface expected by
[toml-test](https://github.com/BurntSushi/toml-test) for my
[toml parser written in Go](https://github.com/BurntSushi/toml).
In particular, it maps TOML data on `stdin` to a JSON format on `stdout`.
Compatible with TOML version
[v0.2.0](https://github.com/mojombo/toml/blob/master/versions/toml-v0.2.0.md)
Compatible with `toml-test` version
[v0.2.0](https://github.com/BurntSushi/toml-test/tree/v0.2.0)
@@ -1,90 +0,0 @@
// Command toml-test-decoder satisfies the toml-test interface for testing
// TOML decoders. Namely, it accepts TOML on stdin and outputs JSON on stdout.
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"path"
"time"
"github.com/BurntSushi/toml"
)
func init() {
log.SetFlags(0)
flag.Usage = usage
flag.Parse()
}
func usage() {
log.Printf("Usage: %s < toml-file\n", path.Base(os.Args[0]))
flag.PrintDefaults()
os.Exit(1)
}
func main() {
if flag.NArg() != 0 {
flag.Usage()
}
var tmp interface{}
if _, err := toml.DecodeReader(os.Stdin, &tmp); err != nil {
log.Fatalf("Error decoding TOML: %s", err)
}
typedTmp := translate(tmp)
if err := json.NewEncoder(os.Stdout).Encode(typedTmp); err != nil {
log.Fatalf("Error encoding JSON: %s", err)
}
}
func translate(tomlData interface{}) interface{} {
switch orig := tomlData.(type) {
case map[string]interface{}:
typed := make(map[string]interface{}, len(orig))
for k, v := range orig {
typed[k] = translate(v)
}
return typed
case []map[string]interface{}:
typed := make([]map[string]interface{}, len(orig))
for i, v := range orig {
typed[i] = translate(v).(map[string]interface{})
}
return typed
case []interface{}:
typed := make([]interface{}, len(orig))
for i, v := range orig {
typed[i] = translate(v)
}
// We don't really need to tag arrays, but let's be future proof.
// (If TOML ever supports tuples, we'll need this.)
return tag("array", typed)
case time.Time:
return tag("datetime", orig.Format("2006-01-02T15:04:05Z"))
case bool:
return tag("bool", fmt.Sprintf("%v", orig))
case int64:
return tag("integer", fmt.Sprintf("%d", orig))
case float64:
return tag("float", fmt.Sprintf("%v", orig))
case string:
return tag("string", orig)
}
panic(fmt.Sprintf("Unknown type: %T", tomlData))
}
func tag(typeName string, data interface{}) map[string]interface{} {
return map[string]interface{}{
"type": typeName,
"value": data,
}
}
@@ -1,14 +0,0 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
@@ -1,14 +0,0 @@
# Implements the TOML test suite interface for TOML encoders
This is an implementation of the interface expected by
[toml-test](https://github.com/BurntSushi/toml-test) for the
[TOML encoder](https://github.com/BurntSushi/toml).
In particular, it maps JSON data on `stdin` to a TOML format on `stdout`.
Compatible with TOML version
[v0.2.0](https://github.com/mojombo/toml/blob/master/versions/toml-v0.2.0.md)
Compatible with `toml-test` version
[v0.2.0](https://github.com/BurntSushi/toml-test/tree/v0.2.0)
@@ -1,131 +0,0 @@
// Command toml-test-encoder satisfies the toml-test interface for testing
// TOML encoders. Namely, it accepts JSON on stdin and outputs TOML on stdout.
package main
import (
"encoding/json"
"flag"
"log"
"os"
"path"
"strconv"
"time"
"github.com/BurntSushi/toml"
)
func init() {
log.SetFlags(0)
flag.Usage = usage
flag.Parse()
}
func usage() {
log.Printf("Usage: %s < json-file\n", path.Base(os.Args[0]))
flag.PrintDefaults()
os.Exit(1)
}
func main() {
if flag.NArg() != 0 {
flag.Usage()
}
var tmp interface{}
if err := json.NewDecoder(os.Stdin).Decode(&tmp); err != nil {
log.Fatalf("Error decoding JSON: %s", err)
}
tomlData := translate(tmp)
if err := toml.NewEncoder(os.Stdout).Encode(tomlData); err != nil {
log.Fatalf("Error encoding TOML: %s", err)
}
}
func translate(typedJson interface{}) interface{} {
switch v := typedJson.(type) {
case map[string]interface{}:
if len(v) == 2 && in("type", v) && in("value", v) {
return untag(v)
}
m := make(map[string]interface{}, len(v))
for k, v2 := range v {
m[k] = translate(v2)
}
return m
case []interface{}:
tabArray := make([]map[string]interface{}, len(v))
for i := range v {
if m, ok := translate(v[i]).(map[string]interface{}); ok {
tabArray[i] = m
} else {
log.Fatalf("JSON arrays may only contain objects. This " +
"corresponds to only tables being allowed in " +
"TOML table arrays.")
}
}
return tabArray
}
log.Fatalf("Unrecognized JSON format '%T'.", typedJson)
panic("unreachable")
}
func untag(typed map[string]interface{}) interface{} {
t := typed["type"].(string)
v := typed["value"]
switch t {
case "string":
return v.(string)
case "integer":
v := v.(string)
n, err := strconv.Atoi(v)
if err != nil {
log.Fatalf("Could not parse '%s' as integer: %s", v, err)
}
return n
case "float":
v := v.(string)
f, err := strconv.ParseFloat(v, 64)
if err != nil {
log.Fatalf("Could not parse '%s' as float64: %s", v, err)
}
return f
case "datetime":
v := v.(string)
t, err := time.Parse("2006-01-02T15:04:05Z", v)
if err != nil {
log.Fatalf("Could not parse '%s' as a datetime: %s", v, err)
}
return t
case "bool":
v := v.(string)
switch v {
case "true":
return true
case "false":
return false
}
log.Fatalf("Could not parse '%s' as a boolean.", v)
case "array":
v := v.([]interface{})
array := make([]interface{}, len(v))
for i := range v {
if m, ok := v[i].(map[string]interface{}); ok {
array[i] = untag(m)
} else {
log.Fatalf("Arrays may only contain other arrays or "+
"primitive values, but found a '%T'.", m)
}
}
return array
}
log.Fatalf("Unrecognized tag type '%s'.", t)
panic("unreachable")
}
func in(key string, m map[string]interface{}) bool {
_, ok := m[key]
return ok
}
-14
View File
@@ -1,14 +0,0 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
-22
View File
@@ -1,22 +0,0 @@
# TOML Validator
If Go is installed, it's simple to try it out:
```bash
go get github.com/BurntSushi/toml/cmd/tomlv
tomlv some-toml-file.toml
```
You can see the types of every key in a TOML file with:
```bash
tomlv -types some-toml-file.toml
```
At the moment, only one error message is reported at a time. Error messages
include line numbers. No output means that the files given are valid TOML, or
there is a bug in `tomlv`.
Compatible with TOML version
[v0.1.0](https://github.com/mojombo/toml/blob/master/versions/toml-v0.1.0.md)
-61
View File
@@ -1,61 +0,0 @@
// Command tomlv validates TOML documents and prints each key's type.
package main
import (
"flag"
"fmt"
"log"
"os"
"path"
"strings"
"text/tabwriter"
"github.com/BurntSushi/toml"
)
var (
flagTypes = false
)
func init() {
log.SetFlags(0)
flag.BoolVar(&flagTypes, "types", flagTypes,
"When set, the types of every defined key will be shown.")
flag.Usage = usage
flag.Parse()
}
func usage() {
log.Printf("Usage: %s toml-file [ toml-file ... ]\n",
path.Base(os.Args[0]))
flag.PrintDefaults()
os.Exit(1)
}
func main() {
if flag.NArg() < 1 {
flag.Usage()
}
for _, f := range flag.Args() {
var tmp interface{}
md, err := toml.DecodeFile(f, &tmp)
if err != nil {
log.Fatalf("Error in '%s': %s", f, err)
}
if flagTypes {
printTypes(md)
}
}
}
func printTypes(md toml.MetaData) {
tabw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
for _, key := range md.Keys() {
fmt.Fprintf(tabw, "%s%s\t%s\n",
strings.Repeat(" ", len(key)-1), key, md.Type(key...))
}
tabw.Flush()
}
-492
View File
@@ -1,492 +0,0 @@
package toml
import (
"fmt"
"io"
"io/ioutil"
"math"
"reflect"
"strings"
"time"
)
var e = fmt.Errorf
// Unmarshaler is the interface implemented by objects that can unmarshal a
// TOML description of themselves.
type Unmarshaler interface {
UnmarshalTOML(interface{}) error
}
// Unmarshal decodes the contents of `p` in TOML format into a pointer `v`.
func Unmarshal(p []byte, v interface{}) error {
_, err := Decode(string(p), v)
return err
}
// Primitive is a TOML value that hasn't been decoded into a Go value.
// When using the various `Decode*` functions, the type `Primitive` may
// be given to any value, and its decoding will be delayed.
//
// A `Primitive` value can be decoded using the `PrimitiveDecode` function.
//
// The underlying representation of a `Primitive` value is subject to change.
// Do not rely on it.
//
// N.B. Primitive values are still parsed, so using them will only avoid
// the overhead of reflection. They can be useful when you don't know the
// exact type of TOML data until run time.
type Primitive struct {
undecoded interface{}
context Key
}
// DEPRECATED!
//
// Use MetaData.PrimitiveDecode instead.
func PrimitiveDecode(primValue Primitive, v interface{}) error {
md := MetaData{decoded: make(map[string]bool)}
return md.unify(primValue.undecoded, rvalue(v))
}
// PrimitiveDecode is just like the other `Decode*` functions, except it
// decodes a TOML value that has already been parsed. Valid primitive values
// can *only* be obtained from values filled by the decoder functions,
// including this method. (i.e., `v` may contain more `Primitive`
// values.)
//
// Meta data for primitive values is included in the meta data returned by
// the `Decode*` functions with one exception: keys returned by the Undecoded
// method will only reflect keys that were decoded. Namely, any keys hidden
// behind a Primitive will be considered undecoded. Executing this method will
// update the undecoded keys in the meta data. (See the example.)
func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error {
md.context = primValue.context
defer func() { md.context = nil }()
return md.unify(primValue.undecoded, rvalue(v))
}
// Decode will decode the contents of `data` in TOML format into a pointer
// `v`.
//
// TOML hashes correspond to Go structs or maps. (Dealer's choice. They can be
// used interchangeably.)
//
// TOML arrays of tables correspond to either a slice of structs or a slice
// of maps.
//
// TOML datetimes correspond to Go `time.Time` values.
//
// All other TOML types (float, string, int, bool and array) correspond
// to the obvious Go types.
//
// An exception to the above rules is if a type implements the
// encoding.TextUnmarshaler interface. In this case, any primitive TOML value
// (floats, strings, integers, booleans and datetimes) will be converted to
// a byte string and given to the value's UnmarshalText method. See the
// Unmarshaler example for a demonstration with time duration strings.
//
// Key mapping
//
// TOML keys can map to either keys in a Go map or field names in a Go
// struct. The special `toml` struct tag may be used to map TOML keys to
// struct fields that don't match the key name exactly. (See the example.)
// A case insensitive match to struct names will be tried if an exact match
// can't be found.
//
// The mapping between TOML values and Go values is loose. That is, there
// may exist TOML values that cannot be placed into your representation, and
// there may be parts of your representation that do not correspond to
// TOML values. This loose mapping can be made stricter by using the IsDefined
// and/or Undecoded methods on the MetaData returned.
//
// This decoder will not handle cyclic types. If a cyclic type is passed,
// `Decode` will not terminate.
func Decode(data string, v interface{}) (MetaData, error) {
p, err := parse(data)
if err != nil {
return MetaData{}, err
}
md := MetaData{
p.mapping, p.types, p.ordered,
make(map[string]bool, len(p.ordered)), nil,
}
return md, md.unify(p.mapping, rvalue(v))
}
// DecodeFile is just like Decode, except it will automatically read the
// contents of the file at `fpath` and decode it for you.
func DecodeFile(fpath string, v interface{}) (MetaData, error) {
bs, err := ioutil.ReadFile(fpath)
if err != nil {
return MetaData{}, err
}
return Decode(string(bs), v)
}
// DecodeReader is just like Decode, except it will consume all bytes
// from the reader and decode it for you.
func DecodeReader(r io.Reader, v interface{}) (MetaData, error) {
bs, err := ioutil.ReadAll(r)
if err != nil {
return MetaData{}, err
}
return Decode(string(bs), v)
}
// unify performs a sort of type unification based on the structure of `rv`,
// which is the client representation.
//
// Any type mismatch produces an error. Finding a type that we don't know
// how to handle produces an unsupported type error.
func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
// Special case. Look for a `Primitive` value.
if rv.Type() == reflect.TypeOf((*Primitive)(nil)).Elem() {
// Save the undecoded data and the key context into the primitive
// value.
context := make(Key, len(md.context))
copy(context, md.context)
rv.Set(reflect.ValueOf(Primitive{
undecoded: data,
context: context,
}))
return nil
}
// Special case. Unmarshaler Interface support.
if rv.CanAddr() {
if v, ok := rv.Addr().Interface().(Unmarshaler); ok {
return v.UnmarshalTOML(data)
}
}
// Special case. Handle time.Time values specifically.
// TODO: Remove this code when we decide to drop support for Go 1.1.
// This isn't necessary in Go 1.2 because time.Time satisfies the encoding
// interfaces.
if rv.Type().AssignableTo(rvalue(time.Time{}).Type()) {
return md.unifyDatetime(data, rv)
}
// Special case. Look for a value satisfying the TextUnmarshaler interface.
if v, ok := rv.Interface().(TextUnmarshaler); ok {
return md.unifyText(data, v)
}
// BUG(burntsushi)
// The behavior here is incorrect whenever a Go type satisfies the
// encoding.TextUnmarshaler interface but also corresponds to a TOML
// hash or array. In particular, the unmarshaler should only be applied
// to primitive TOML values. But at this point, it will be applied to
// all kinds of values and produce an incorrect error whenever those values
// are hashes or arrays (including arrays of tables).
k := rv.Kind()
// laziness
if k >= reflect.Int && k <= reflect.Uint64 {
return md.unifyInt(data, rv)
}
switch k {
case reflect.Ptr:
elem := reflect.New(rv.Type().Elem())
err := md.unify(data, reflect.Indirect(elem))
if err != nil {
return err
}
rv.Set(elem)
return nil
case reflect.Struct:
return md.unifyStruct(data, rv)
case reflect.Map:
return md.unifyMap(data, rv)
case reflect.Array:
return md.unifyArray(data, rv)
case reflect.Slice:
return md.unifySlice(data, rv)
case reflect.String:
return md.unifyString(data, rv)
case reflect.Bool:
return md.unifyBool(data, rv)
case reflect.Interface:
// we only support empty interfaces.
if rv.NumMethod() > 0 {
return e("Unsupported type '%s'.", rv.Kind())
}
return md.unifyAnything(data, rv)
case reflect.Float32:
fallthrough
case reflect.Float64:
return md.unifyFloat64(data, rv)
}
return e("Unsupported type '%s'.", rv.Kind())
}
func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
tmap, ok := mapping.(map[string]interface{})
if !ok {
return mismatch(rv, "map", mapping)
}
for key, datum := range tmap {
var f *field
fields := cachedTypeFields(rv.Type())
for i := range fields {
ff := &fields[i]
if ff.name == key {
f = ff
break
}
if f == nil && strings.EqualFold(ff.name, key) {
f = ff
}
}
if f != nil {
subv := rv
for _, i := range f.index {
subv = indirect(subv.Field(i))
}
if isUnifiable(subv) {
md.decoded[md.context.add(key).String()] = true
md.context = append(md.context, key)
if err := md.unify(datum, subv); err != nil {
return e("Type mismatch for '%s.%s': %s",
rv.Type().String(), f.name, err)
}
md.context = md.context[0 : len(md.context)-1]
} else if f.name != "" {
// Bad user! No soup for you!
return e("Field '%s.%s' is unexported, and therefore cannot "+
"be loaded with reflection.", rv.Type().String(), f.name)
}
}
}
return nil
}
func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error {
tmap, ok := mapping.(map[string]interface{})
if !ok {
return badtype("map", mapping)
}
if rv.IsNil() {
rv.Set(reflect.MakeMap(rv.Type()))
}
for k, v := range tmap {
md.decoded[md.context.add(k).String()] = true
md.context = append(md.context, k)
rvkey := indirect(reflect.New(rv.Type().Key()))
rvval := reflect.Indirect(reflect.New(rv.Type().Elem()))
if err := md.unify(v, rvval); err != nil {
return err
}
md.context = md.context[0 : len(md.context)-1]
rvkey.SetString(k)
rv.SetMapIndex(rvkey, rvval)
}
return nil
}
func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error {
datav := reflect.ValueOf(data)
if datav.Kind() != reflect.Slice {
return badtype("slice", data)
}
sliceLen := datav.Len()
if sliceLen != rv.Len() {
return e("expected array length %d; got TOML array of length %d",
rv.Len(), sliceLen)
}
return md.unifySliceArray(datav, rv)
}
func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error {
datav := reflect.ValueOf(data)
if datav.Kind() != reflect.Slice {
return badtype("slice", data)
}
sliceLen := datav.Len()
if rv.IsNil() {
rv.Set(reflect.MakeSlice(rv.Type(), sliceLen, sliceLen))
}
return md.unifySliceArray(datav, rv)
}
func (md *MetaData) unifySliceArray(data, rv reflect.Value) error {
sliceLen := data.Len()
for i := 0; i < sliceLen; i++ {
v := data.Index(i).Interface()
sliceval := indirect(rv.Index(i))
if err := md.unify(v, sliceval); err != nil {
return err
}
}
return nil
}
func (md *MetaData) unifyDatetime(data interface{}, rv reflect.Value) error {
if _, ok := data.(time.Time); ok {
rv.Set(reflect.ValueOf(data))
return nil
}
return badtype("time.Time", data)
}
func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error {
if s, ok := data.(string); ok {
rv.SetString(s)
return nil
}
return badtype("string", data)
}
func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error {
if num, ok := data.(float64); ok {
switch rv.Kind() {
case reflect.Float32:
fallthrough
case reflect.Float64:
rv.SetFloat(num)
default:
panic("bug")
}
return nil
}
return badtype("float", data)
}
func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error {
if num, ok := data.(int64); ok {
if rv.Kind() >= reflect.Int && rv.Kind() <= reflect.Int64 {
switch rv.Kind() {
case reflect.Int, reflect.Int64:
// No bounds checking necessary.
case reflect.Int8:
if num < math.MinInt8 || num > math.MaxInt8 {
return e("Value '%d' is out of range for int8.", num)
}
case reflect.Int16:
if num < math.MinInt16 || num > math.MaxInt16 {
return e("Value '%d' is out of range for int16.", num)
}
case reflect.Int32:
if num < math.MinInt32 || num > math.MaxInt32 {
return e("Value '%d' is out of range for int32.", num)
}
}
rv.SetInt(num)
} else if rv.Kind() >= reflect.Uint && rv.Kind() <= reflect.Uint64 {
unum := uint64(num)
switch rv.Kind() {
case reflect.Uint, reflect.Uint64:
// No bounds checking necessary.
case reflect.Uint8:
if num < 0 || unum > math.MaxUint8 {
return e("Value '%d' is out of range for uint8.", num)
}
case reflect.Uint16:
if num < 0 || unum > math.MaxUint16 {
return e("Value '%d' is out of range for uint16.", num)
}
case reflect.Uint32:
if num < 0 || unum > math.MaxUint32 {
return e("Value '%d' is out of range for uint32.", num)
}
}
rv.SetUint(unum)
} else {
panic("unreachable")
}
return nil
}
return badtype("integer", data)
}
func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error {
if b, ok := data.(bool); ok {
rv.SetBool(b)
return nil
}
return badtype("boolean", data)
}
func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error {
rv.Set(reflect.ValueOf(data))
return nil
}
func (md *MetaData) unifyText(data interface{}, v TextUnmarshaler) error {
var s string
switch sdata := data.(type) {
case TextMarshaler:
text, err := sdata.MarshalText()
if err != nil {
return err
}
s = string(text)
case fmt.Stringer:
s = sdata.String()
case string:
s = sdata
case bool:
s = fmt.Sprintf("%v", sdata)
case int64:
s = fmt.Sprintf("%d", sdata)
case float64:
s = fmt.Sprintf("%f", sdata)
default:
return badtype("primitive (string-like)", data)
}
if err := v.UnmarshalText([]byte(s)); err != nil {
return err
}
return nil
}
// rvalue returns a reflect.Value of `v`. All pointers are resolved.
func rvalue(v interface{}) reflect.Value {
return indirect(reflect.ValueOf(v))
}
// indirect returns the value pointed to by a pointer.
// Pointers are followed until the value is not a pointer.
// New values are allocated for each nil pointer.
//
// An exception to this rule is if the value satisfies an interface of
// interest to us (like encoding.TextUnmarshaler).
func indirect(v reflect.Value) reflect.Value {
if v.Kind() != reflect.Ptr {
if v.CanAddr() {
pv := v.Addr()
if _, ok := pv.Interface().(TextUnmarshaler); ok {
return pv
}
}
return v
}
if v.IsNil() {
v.Set(reflect.New(v.Type().Elem()))
}
return indirect(reflect.Indirect(v))
}
func isUnifiable(rv reflect.Value) bool {
if rv.CanSet() {
return true
}
if _, ok := rv.Interface().(TextUnmarshaler); ok {
return true
}
return false
}
func badtype(expected string, data interface{}) error {
return e("Expected %s but found '%T'.", expected, data)
}
func mismatch(user reflect.Value, expected string, data interface{}) error {
return e("Type mismatch for %s. Expected %s but found '%T'.",
user.Type().String(), expected, data)
}
-99
View File
@@ -1,99 +0,0 @@
package toml
import "strings"
// MetaData allows access to meta information about TOML data that may not
// be inferrable via reflection. In particular, whether a key has been defined
// and the TOML type of a key.
type MetaData struct {
mapping map[string]interface{}
types map[string]tomlType
keys []Key
decoded map[string]bool
context Key // Used only during decoding.
}
// IsDefined returns true if the key given exists in the TOML data. The key
// should be specified hierarchially. e.g.,
//
// // access the TOML key 'a.b.c'
// IsDefined("a", "b", "c")
//
// IsDefined will return false if an empty key given. Keys are case sensitive.
func (md *MetaData) IsDefined(key ...string) bool {
if len(key) == 0 {
return false
}
var hash map[string]interface{}
var ok bool
var hashOrVal interface{} = md.mapping
for _, k := range key {
if hash, ok = hashOrVal.(map[string]interface{}); !ok {
return false
}
if hashOrVal, ok = hash[k]; !ok {
return false
}
}
return true
}
// Type returns a string representation of the type of the key specified.
//
// Type will return the empty string if given an empty key or a key that
// does not exist. Keys are case sensitive.
func (md *MetaData) Type(key ...string) string {
fullkey := strings.Join(key, ".")
if typ, ok := md.types[fullkey]; ok {
return typ.typeString()
}
return ""
}
// Key is the type of any TOML key, including key groups. Use (MetaData).Keys
// to get values of this type.
type Key []string
func (k Key) String() string {
return strings.Join(k, ".")
}
func (k Key) add(piece string) Key {
newKey := make(Key, len(k)+1)
copy(newKey, k)
newKey[len(k)] = piece
return newKey
}
// Keys returns a slice of every key in the TOML data, including key groups.
// Each key is itself a slice, where the first element is the top of the
// hierarchy and the last is the most specific.
//
// The list will have the same order as the keys appeared in the TOML data.
//
// All keys returned are non-empty.
func (md *MetaData) Keys() []Key {
return md.keys
}
// Undecoded returns all keys that have not been decoded in the order in which
// they appear in the original TOML document.
//
// This includes keys that haven't been decoded because of a Primitive value.
// Once the Primitive value is decoded, the keys will be considered decoded.
//
// Also note that decoding into an empty interface will result in no decoding,
// and so no keys will be considered decoded.
//
// In this sense, the Undecoded keys correspond to keys in the TOML document
// that do not have a concrete type in your representation.
func (md *MetaData) Undecoded() []Key {
undecoded := make([]Key, 0, len(md.keys))
for _, key := range md.keys {
if !md.decoded[key.String()] {
undecoded = append(undecoded, key)
}
}
return undecoded
}
-949
View File
@@ -1,949 +0,0 @@
package toml
import (
"fmt"
"log"
"reflect"
"testing"
"time"
)
func init() {
log.SetFlags(0)
}
func TestDecodeSimple(t *testing.T) {
var testSimple = `
age = 250
andrew = "gallant"
kait = "brady"
now = 1987-07-05T05:45:00Z
yesOrNo = true
pi = 3.14
colors = [
["red", "green", "blue"],
["cyan", "magenta", "yellow", "black"],
]
[My.Cats]
plato = "cat 1"
cauchy = "cat 2"
`
type cats struct {
Plato string
Cauchy string
}
type simple struct {
Age int
Colors [][]string
Pi float64
YesOrNo bool
Now time.Time
Andrew string
Kait string
My map[string]cats
}
var val simple
_, err := Decode(testSimple, &val)
if err != nil {
t.Fatal(err)
}
now, err := time.Parse("2006-01-02T15:04:05", "1987-07-05T05:45:00")
if err != nil {
panic(err)
}
var answer = simple{
Age: 250,
Andrew: "gallant",
Kait: "brady",
Now: now,
YesOrNo: true,
Pi: 3.14,
Colors: [][]string{
{"red", "green", "blue"},
{"cyan", "magenta", "yellow", "black"},
},
My: map[string]cats{
"Cats": cats{Plato: "cat 1", Cauchy: "cat 2"},
},
}
if !reflect.DeepEqual(val, answer) {
t.Fatalf("Expected\n-----\n%#v\n-----\nbut got\n-----\n%#v\n",
answer, val)
}
}
func TestDecodeEmbedded(t *testing.T) {
type Dog struct{ Name string }
type Age int
tests := map[string]struct {
input string
decodeInto interface{}
wantDecoded interface{}
}{
"embedded struct": {
input: `Name = "milton"`,
decodeInto: &struct{ Dog }{},
wantDecoded: &struct{ Dog }{Dog{"milton"}},
},
"embedded non-nil pointer to struct": {
input: `Name = "milton"`,
decodeInto: &struct{ *Dog }{},
wantDecoded: &struct{ *Dog }{&Dog{"milton"}},
},
"embedded nil pointer to struct": {
input: ``,
decodeInto: &struct{ *Dog }{},
wantDecoded: &struct{ *Dog }{nil},
},
"embedded int": {
input: `Age = -5`,
decodeInto: &struct{ Age }{},
wantDecoded: &struct{ Age }{-5},
},
}
for label, test := range tests {
_, err := Decode(test.input, test.decodeInto)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(test.wantDecoded, test.decodeInto) {
t.Errorf("%s: want decoded == %+v, got %+v",
label, test.wantDecoded, test.decodeInto)
}
}
}
func TestTableArrays(t *testing.T) {
var tomlTableArrays = `
[[albums]]
name = "Born to Run"
[[albums.songs]]
name = "Jungleland"
[[albums.songs]]
name = "Meeting Across the River"
[[albums]]
name = "Born in the USA"
[[albums.songs]]
name = "Glory Days"
[[albums.songs]]
name = "Dancing in the Dark"
`
type Song struct {
Name string
}
type Album struct {
Name string
Songs []Song
}
type Music struct {
Albums []Album
}
expected := Music{[]Album{
{"Born to Run", []Song{{"Jungleland"}, {"Meeting Across the River"}}},
{"Born in the USA", []Song{{"Glory Days"}, {"Dancing in the Dark"}}},
}}
var got Music
if _, err := Decode(tomlTableArrays, &got); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(expected, got) {
t.Fatalf("\n%#v\n!=\n%#v\n", expected, got)
}
}
// Case insensitive matching tests.
// A bit more comprehensive than needed given the current implementation,
// but implementations change.
// Probably still missing demonstrations of some ugly corner cases regarding
// case insensitive matching and multiple fields.
func TestCase(t *testing.T) {
var caseToml = `
tOpString = "string"
tOpInt = 1
tOpFloat = 1.1
tOpBool = true
tOpdate = 2006-01-02T15:04:05Z
tOparray = [ "array" ]
Match = "i should be in Match only"
MatcH = "i should be in MatcH only"
once = "just once"
[nEst.eD]
nEstedString = "another string"
`
type InsensitiveEd struct {
NestedString string
}
type InsensitiveNest struct {
Ed InsensitiveEd
}
type Insensitive struct {
TopString string
TopInt int
TopFloat float64
TopBool bool
TopDate time.Time
TopArray []string
Match string
MatcH string
Once string
OncE string
Nest InsensitiveNest
}
tme, err := time.Parse(time.RFC3339, time.RFC3339[:len(time.RFC3339)-5])
if err != nil {
panic(err)
}
expected := Insensitive{
TopString: "string",
TopInt: 1,
TopFloat: 1.1,
TopBool: true,
TopDate: tme,
TopArray: []string{"array"},
MatcH: "i should be in MatcH only",
Match: "i should be in Match only",
Once: "just once",
OncE: "",
Nest: InsensitiveNest{
Ed: InsensitiveEd{NestedString: "another string"},
},
}
var got Insensitive
if _, err := Decode(caseToml, &got); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(expected, got) {
t.Fatalf("\n%#v\n!=\n%#v\n", expected, got)
}
}
func TestPointers(t *testing.T) {
type Object struct {
Type string
Description string
}
type Dict struct {
NamedObject map[string]*Object
BaseObject *Object
Strptr *string
Strptrs []*string
}
s1, s2, s3 := "blah", "abc", "def"
expected := &Dict{
Strptr: &s1,
Strptrs: []*string{&s2, &s3},
NamedObject: map[string]*Object{
"foo": {"FOO", "fooooo!!!"},
"bar": {"BAR", "ba-ba-ba-ba-barrrr!!!"},
},
BaseObject: &Object{"BASE", "da base"},
}
ex1 := `
Strptr = "blah"
Strptrs = ["abc", "def"]
[NamedObject.foo]
Type = "FOO"
Description = "fooooo!!!"
[NamedObject.bar]
Type = "BAR"
Description = "ba-ba-ba-ba-barrrr!!!"
[BaseObject]
Type = "BASE"
Description = "da base"
`
dict := new(Dict)
_, err := Decode(ex1, dict)
if err != nil {
t.Errorf("Decode error: %v", err)
}
if !reflect.DeepEqual(expected, dict) {
t.Fatalf("\n%#v\n!=\n%#v\n", expected, dict)
}
}
type sphere struct {
Center [3]float64
Radius float64
}
func TestDecodeSimpleArray(t *testing.T) {
var s1 sphere
if _, err := Decode(`center = [0.0, 1.5, 0.0]`, &s1); err != nil {
t.Fatal(err)
}
}
func TestDecodeArrayWrongSize(t *testing.T) {
var s1 sphere
if _, err := Decode(`center = [0.1, 2.3]`, &s1); err == nil {
t.Fatal("Expected array type mismatch error")
}
}
func TestDecodeLargeIntoSmallInt(t *testing.T) {
type table struct {
Value int8
}
var tab table
if _, err := Decode(`value = 500`, &tab); err == nil {
t.Fatal("Expected integer out-of-bounds error.")
}
}
func TestDecodeSizedInts(t *testing.T) {
type table struct {
U8 uint8
U16 uint16
U32 uint32
U64 uint64
U uint
I8 int8
I16 int16
I32 int32
I64 int64
I int
}
answer := table{1, 1, 1, 1, 1, -1, -1, -1, -1, -1}
toml := `
u8 = 1
u16 = 1
u32 = 1
u64 = 1
u = 1
i8 = -1
i16 = -1
i32 = -1
i64 = -1
i = -1
`
var tab table
if _, err := Decode(toml, &tab); err != nil {
t.Fatal(err.Error())
}
if answer != tab {
t.Fatalf("Expected %#v but got %#v", answer, tab)
}
}
func TestUnmarshaler(t *testing.T) {
var tomlBlob = `
[dishes.hamboogie]
name = "Hamboogie with fries"
price = 10.99
[[dishes.hamboogie.ingredients]]
name = "Bread Bun"
[[dishes.hamboogie.ingredients]]
name = "Lettuce"
[[dishes.hamboogie.ingredients]]
name = "Real Beef Patty"
[[dishes.hamboogie.ingredients]]
name = "Tomato"
[dishes.eggsalad]
name = "Egg Salad with rice"
price = 3.99
[[dishes.eggsalad.ingredients]]
name = "Egg"
[[dishes.eggsalad.ingredients]]
name = "Mayo"
[[dishes.eggsalad.ingredients]]
name = "Rice"
`
m := &menu{}
if _, err := Decode(tomlBlob, m); err != nil {
log.Fatal(err)
}
if len(m.Dishes) != 2 {
t.Log("two dishes should be loaded with UnmarshalTOML()")
t.Errorf("expected %d but got %d", 2, len(m.Dishes))
}
eggSalad := m.Dishes["eggsalad"]
if _, ok := interface{}(eggSalad).(dish); !ok {
t.Errorf("expected a dish")
}
if eggSalad.Name != "Egg Salad with rice" {
t.Errorf("expected the dish to be named 'Egg Salad with rice'")
}
if len(eggSalad.Ingredients) != 3 {
t.Log("dish should be loaded with UnmarshalTOML()")
t.Errorf("expected %d but got %d", 3, len(eggSalad.Ingredients))
}
found := false
for _, i := range eggSalad.Ingredients {
if i.Name == "Rice" {
found = true
break
}
}
if !found {
t.Error("Rice was not loaded in UnmarshalTOML()")
}
// test on a value - must be passed as *
o := menu{}
if _, err := Decode(tomlBlob, &o); err != nil {
log.Fatal(err)
}
}
type menu struct {
Dishes map[string]dish
}
func (m *menu) UnmarshalTOML(p interface{}) error {
m.Dishes = make(map[string]dish)
data, _ := p.(map[string]interface{})
dishes := data["dishes"].(map[string]interface{})
for n, v := range dishes {
if d, ok := v.(map[string]interface{}); ok {
nd := dish{}
nd.UnmarshalTOML(d)
m.Dishes[n] = nd
} else {
return fmt.Errorf("not a dish")
}
}
return nil
}
type dish struct {
Name string
Price float32
Ingredients []ingredient
}
func (d *dish) UnmarshalTOML(p interface{}) error {
data, _ := p.(map[string]interface{})
d.Name, _ = data["name"].(string)
d.Price, _ = data["price"].(float32)
ingredients, _ := data["ingredients"].([]map[string]interface{})
for _, e := range ingredients {
n, _ := interface{}(e).(map[string]interface{})
name, _ := n["name"].(string)
i := ingredient{name}
d.Ingredients = append(d.Ingredients, i)
}
return nil
}
type ingredient struct {
Name string
}
func ExampleMetaData_PrimitiveDecode() {
var md MetaData
var err error
var tomlBlob = `
ranking = ["Springsteen", "J Geils"]
[bands.Springsteen]
started = 1973
albums = ["Greetings", "WIESS", "Born to Run", "Darkness"]
[bands.J Geils]
started = 1970
albums = ["The J. Geils Band", "Full House", "Blow Your Face Out"]
`
type band struct {
Started int
Albums []string
}
type classics struct {
Ranking []string
Bands map[string]Primitive
}
// Do the initial decode. Reflection is delayed on Primitive values.
var music classics
if md, err = Decode(tomlBlob, &music); err != nil {
log.Fatal(err)
}
// MetaData still includes information on Primitive values.
fmt.Printf("Is `bands.Springsteen` defined? %v\n",
md.IsDefined("bands", "Springsteen"))
// Decode primitive data into Go values.
for _, artist := range music.Ranking {
// A band is a primitive value, so we need to decode it to get a
// real `band` value.
primValue := music.Bands[artist]
var aBand band
if err = md.PrimitiveDecode(primValue, &aBand); err != nil {
log.Fatal(err)
}
fmt.Printf("%s started in %d.\n", artist, aBand.Started)
}
// Check to see if there were any fields left undecoded.
// Note that this won't be empty before decoding the Primitive value!
fmt.Printf("Undecoded: %q\n", md.Undecoded())
// Output:
// Is `bands.Springsteen` defined? true
// Springsteen started in 1973.
// J Geils started in 1970.
// Undecoded: []
}
func ExampleDecode() {
var tomlBlob = `
# Some comments.
[alpha]
ip = "10.0.0.1"
[alpha.config]
Ports = [ 8001, 8002 ]
Location = "Toronto"
Created = 1987-07-05T05:45:00Z
[beta]
ip = "10.0.0.2"
[beta.config]
Ports = [ 9001, 9002 ]
Location = "New Jersey"
Created = 1887-01-05T05:55:00Z
`
type serverConfig struct {
Ports []int
Location string
Created time.Time
}
type server struct {
IP string `toml:"ip"`
Config serverConfig `toml:"config"`
}
type servers map[string]server
var config servers
if _, err := Decode(tomlBlob, &config); err != nil {
log.Fatal(err)
}
for _, name := range []string{"alpha", "beta"} {
s := config[name]
fmt.Printf("Server: %s (ip: %s) in %s created on %s\n",
name, s.IP, s.Config.Location,
s.Config.Created.Format("2006-01-02"))
fmt.Printf("Ports: %v\n", s.Config.Ports)
}
// Output:
// Server: alpha (ip: 10.0.0.1) in Toronto created on 1987-07-05
// Ports: [8001 8002]
// Server: beta (ip: 10.0.0.2) in New Jersey created on 1887-01-05
// Ports: [9001 9002]
}
type duration struct {
time.Duration
}
func (d *duration) UnmarshalText(text []byte) error {
var err error
d.Duration, err = time.ParseDuration(string(text))
return err
}
// Example Unmarshaler shows how to decode TOML strings into your own
// custom data type.
func Example_unmarshaler() {
blob := `
[[song]]
name = "Thunder Road"
duration = "4m49s"
[[song]]
name = "Stairway to Heaven"
duration = "8m03s"
`
type song struct {
Name string
Duration duration
}
type songs struct {
Song []song
}
var favorites songs
if _, err := Decode(blob, &favorites); err != nil {
log.Fatal(err)
}
// Code to implement the TextUnmarshaler interface for `duration`:
//
// type duration struct {
// time.Duration
// }
//
// func (d *duration) UnmarshalText(text []byte) error {
// var err error
// d.Duration, err = time.ParseDuration(string(text))
// return err
// }
for _, s := range favorites.Song {
fmt.Printf("%s (%s)\n", s.Name, s.Duration)
}
// Output:
// Thunder Road (4m49s)
// Stairway to Heaven (8m3s)
}
// Example StrictDecoding shows how to detect whether there are keys in the
// TOML document that weren't decoded into the value given. This is useful
// for returning an error to the user if they've included extraneous fields
// in their configuration.
func Example_strictDecoding() {
var blob = `
key1 = "value1"
key2 = "value2"
key3 = "value3"
`
type config struct {
Key1 string
Key3 string
}
var conf config
md, err := Decode(blob, &conf)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Undecoded keys: %q\n", md.Undecoded())
// Output:
// Undecoded keys: ["key2"]
}
// Example UnmarshalTOML shows how to implement a struct type that knows how to
// unmarshal itself. The struct must take full responsibility for mapping the
// values passed into the struct. The method may be used with interfaces in a
// struct in cases where the actual type is not known until the data is examined.
func Example_unmarshalTOML() {
var blob = `
[[parts]]
type = "valve"
id = "valve-1"
size = 1.2
rating = 4
[[parts]]
type = "valve"
id = "valve-2"
size = 2.1
rating = 5
[[parts]]
type = "pipe"
id = "pipe-1"
length = 2.1
diameter = 12
[[parts]]
type = "cable"
id = "cable-1"
length = 12
rating = 3.1
`
o := &order{}
err := Unmarshal([]byte(blob), o)
if err != nil {
log.Fatal(err)
}
fmt.Println(len(o.parts))
for _, part := range o.parts {
fmt.Println(part.Name())
}
// Code to implement UmarshalJSON.
// type order struct {
// // NOTE `order.parts` is a private slice of type `part` which is an
// // interface and may only be loaded from toml using the UnmarshalTOML()
// // method of the Umarshaler interface.
// parts parts
// }
// func (o *order) UnmarshalTOML(data interface{}) error {
// // NOTE the example below contains detailed type casting to show how
// // the 'data' is retrieved. In operational use, a type cast wrapper
// // may be prefered e.g.
// //
// // func AsMap(v interface{}) (map[string]interface{}, error) {
// // return v.(map[string]interface{})
// // }
// //
// // resulting in:
// // d, _ := AsMap(data)
// //
// d, _ := data.(map[string]interface{})
// parts, _ := d["parts"].([]map[string]interface{})
// for _, p := range parts {
// typ, _ := p["type"].(string)
// id, _ := p["id"].(string)
// // detect the type of part and handle each case
// switch p["type"] {
// case "valve":
// size := float32(p["size"].(float64))
// rating := int(p["rating"].(int64))
// valve := &valve{
// Type: typ,
// ID: id,
// Size: size,
// Rating: rating,
// }
// o.parts = append(o.parts, valve)
// case "pipe":
// length := float32(p["length"].(float64))
// diameter := int(p["diameter"].(int64))
// pipe := &pipe{
// Type: typ,
// ID: id,
// Length: length,
// Diameter: diameter,
// }
// o.parts = append(o.parts, pipe)
// case "cable":
// length := int(p["length"].(int64))
// rating := float32(p["rating"].(float64))
// cable := &cable{
// Type: typ,
// ID: id,
// Length: length,
// Rating: rating,
// }
// o.parts = append(o.parts, cable)
// }
// }
// return nil
// }
// type parts []part
// type part interface {
// Name() string
// }
// type valve struct {
// Type string
// ID string
// Size float32
// Rating int
// }
// func (v *valve) Name() string {
// return fmt.Sprintf("VALVE: %s", v.ID)
// }
// type pipe struct {
// Type string
// ID string
// Length float32
// Diameter int
// }
// func (p *pipe) Name() string {
// return fmt.Sprintf("PIPE: %s", p.ID)
// }
// type cable struct {
// Type string
// ID string
// Length int
// Rating float32
// }
// func (c *cable) Name() string {
// return fmt.Sprintf("CABLE: %s", c.ID)
// }
// Output:
// 4
// VALVE: valve-1
// VALVE: valve-2
// PIPE: pipe-1
// CABLE: cable-1
}
type order struct {
// NOTE `order.parts` is a private slice of type `part` which is an
// interface and may only be loaded from toml using the UnmarshalTOML()
// method of the Umarshaler interface.
parts parts
}
func (o *order) UnmarshalTOML(data interface{}) error {
// NOTE the example below contains detailed type casting to show how
// the 'data' is retrieved. In operational use, a type cast wrapper
// may be prefered e.g.
//
// func AsMap(v interface{}) (map[string]interface{}, error) {
// return v.(map[string]interface{})
// }
//
// resulting in:
// d, _ := AsMap(data)
//
d, _ := data.(map[string]interface{})
parts, _ := d["parts"].([]map[string]interface{})
for _, p := range parts {
typ, _ := p["type"].(string)
id, _ := p["id"].(string)
// detect the type of part and handle each case
switch p["type"] {
case "valve":
size := float32(p["size"].(float64))
rating := int(p["rating"].(int64))
valve := &valve{
Type: typ,
ID: id,
Size: size,
Rating: rating,
}
o.parts = append(o.parts, valve)
case "pipe":
length := float32(p["length"].(float64))
diameter := int(p["diameter"].(int64))
pipe := &pipe{
Type: typ,
ID: id,
Length: length,
Diameter: diameter,
}
o.parts = append(o.parts, pipe)
case "cable":
length := int(p["length"].(int64))
rating := float32(p["rating"].(float64))
cable := &cable{
Type: typ,
ID: id,
Length: length,
Rating: rating,
}
o.parts = append(o.parts, cable)
}
}
return nil
}
type parts []part
type part interface {
Name() string
}
type valve struct {
Type string
ID string
Size float32
Rating int
}
func (v *valve) Name() string {
return fmt.Sprintf("VALVE: %s", v.ID)
}
type pipe struct {
Type string
ID string
Length float32
Diameter int
}
func (p *pipe) Name() string {
return fmt.Sprintf("PIPE: %s", p.ID)
}
type cable struct {
Type string
ID string
Length int
Rating float32
}
func (c *cable) Name() string {
return fmt.Sprintf("CABLE: %s", c.ID)
}
-27
View File
@@ -1,27 +0,0 @@
/*
Package toml provides facilities for decoding and encoding TOML configuration
files via reflection. There is also support for delaying decoding with
the Primitive type, and querying the set of keys in a TOML document with the
MetaData type.
The specification implemented: https://github.com/mojombo/toml
The sub-command github.com/BurntSushi/toml/cmd/tomlv can be used to verify
whether a file is a valid TOML document. It can also be used to print the
type of each key in a TOML document.
Testing
There are two important types of tests used for this package. The first is
contained inside '*_test.go' files and uses the standard Go unit testing
framework. These tests are primarily devoted to holistically testing the
decoder and encoder.
The second type of testing is used to verify the implementation's adherence
to the TOML specification. These tests have been factored into their own
project: https://github.com/BurntSushi/toml-test
The reason the tests are in a separate project is so that they can be used by
any implementation of TOML. Namely, it is language agnostic.
*/
package toml
-515
View File
@@ -1,515 +0,0 @@
package toml
import (
"bufio"
"errors"
"fmt"
"io"
"reflect"
"sort"
"strconv"
"strings"
"time"
)
type tomlEncodeError struct{ error }
var (
errArrayMixedElementTypes = errors.New(
"can't encode array with mixed element types")
errArrayNilElement = errors.New(
"can't encode array with nil element")
errNonString = errors.New(
"can't encode a map with non-string key type")
errAnonNonStruct = errors.New(
"can't encode an anonymous field that is not a struct")
errArrayNoTable = errors.New(
"TOML array element can't contain a table")
errNoKey = errors.New(
"top-level values must be a Go map or struct")
errAnything = errors.New("") // used in testing
)
var quotedReplacer = strings.NewReplacer(
"\t", "\\t",
"\n", "\\n",
"\r", "\\r",
"\"", "\\\"",
"\\", "\\\\",
)
// Encoder controls the encoding of Go values to a TOML document to some
// io.Writer.
//
// The indentation level can be controlled with the Indent field.
type Encoder struct {
// A single indentation level. By default it is two spaces.
Indent string
// hasWritten is whether we have written any output to w yet.
hasWritten bool
w *bufio.Writer
}
// NewEncoder returns a TOML encoder that encodes Go values to the io.Writer
// given. By default, a single indentation level is 2 spaces.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{
w: bufio.NewWriter(w),
Indent: " ",
}
}
// Encode writes a TOML representation of the Go value to the underlying
// io.Writer. If the value given cannot be encoded to a valid TOML document,
// then an error is returned.
//
// The mapping between Go values and TOML values should be precisely the same
// as for the Decode* functions. Similarly, the TextMarshaler interface is
// supported by encoding the resulting bytes as strings. (If you want to write
// arbitrary binary data then you will need to use something like base64 since
// TOML does not have any binary types.)
//
// When encoding TOML hashes (i.e., Go maps or structs), keys without any
// sub-hashes are encoded first.
//
// If a Go map is encoded, then its keys are sorted alphabetically for
// deterministic output. More control over this behavior may be provided if
// there is demand for it.
//
// Encoding Go values without a corresponding TOML representation---like map
// types with non-string keys---will cause an error to be returned. Similarly
// for mixed arrays/slices, arrays/slices with nil elements, embedded
// non-struct types and nested slices containing maps or structs.
// (e.g., [][]map[string]string is not allowed but []map[string]string is OK
// and so is []map[string][]string.)
func (enc *Encoder) Encode(v interface{}) error {
rv := eindirect(reflect.ValueOf(v))
if err := enc.safeEncode(Key([]string{}), rv); err != nil {
return err
}
return enc.w.Flush()
}
func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) {
defer func() {
if r := recover(); r != nil {
if terr, ok := r.(tomlEncodeError); ok {
err = terr.error
return
}
panic(r)
}
}()
enc.encode(key, rv)
return nil
}
func (enc *Encoder) encode(key Key, rv reflect.Value) {
// Special case. Time needs to be in ISO8601 format.
// Special case. If we can marshal the type to text, then we used that.
// Basically, this prevents the encoder for handling these types as
// generic structs (or whatever the underlying type of a TextMarshaler is).
switch rv.Interface().(type) {
case time.Time, TextMarshaler:
enc.keyEqElement(key, rv)
return
}
k := rv.Kind()
switch k {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64,
reflect.Float32, reflect.Float64, reflect.String, reflect.Bool:
enc.keyEqElement(key, rv)
case reflect.Array, reflect.Slice:
if typeEqual(tomlArrayHash, tomlTypeOfGo(rv)) {
enc.eArrayOfTables(key, rv)
} else {
enc.keyEqElement(key, rv)
}
case reflect.Interface:
if rv.IsNil() {
return
}
enc.encode(key, rv.Elem())
case reflect.Map:
if rv.IsNil() {
return
}
enc.eTable(key, rv)
case reflect.Ptr:
if rv.IsNil() {
return
}
enc.encode(key, rv.Elem())
case reflect.Struct:
enc.eTable(key, rv)
default:
panic(e("Unsupported type for key '%s': %s", key, k))
}
}
// eElement encodes any value that can be an array element (primitives and
// arrays).
func (enc *Encoder) eElement(rv reflect.Value) {
switch v := rv.Interface().(type) {
case time.Time:
// Special case time.Time as a primitive. Has to come before
// TextMarshaler below because time.Time implements
// encoding.TextMarshaler, but we need to always use UTC.
enc.wf(v.In(time.FixedZone("UTC", 0)).Format("2006-01-02T15:04:05Z"))
return
case TextMarshaler:
// Special case. Use text marshaler if it's available for this value.
if s, err := v.MarshalText(); err != nil {
encPanic(err)
} else {
enc.writeQuoted(string(s))
}
return
}
switch rv.Kind() {
case reflect.Bool:
enc.wf(strconv.FormatBool(rv.Bool()))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
enc.wf(strconv.FormatInt(rv.Int(), 10))
case reflect.Uint, reflect.Uint8, reflect.Uint16,
reflect.Uint32, reflect.Uint64:
enc.wf(strconv.FormatUint(rv.Uint(), 10))
case reflect.Float32:
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 32)))
case reflect.Float64:
enc.wf(floatAddDecimal(strconv.FormatFloat(rv.Float(), 'f', -1, 64)))
case reflect.Array, reflect.Slice:
enc.eArrayOrSliceElement(rv)
case reflect.Interface:
enc.eElement(rv.Elem())
case reflect.String:
enc.writeQuoted(rv.String())
default:
panic(e("Unexpected primitive type: %s", rv.Kind()))
}
}
// By the TOML spec, all floats must have a decimal with at least one
// number on either side.
func floatAddDecimal(fstr string) string {
if !strings.Contains(fstr, ".") {
return fstr + ".0"
}
return fstr
}
func (enc *Encoder) writeQuoted(s string) {
enc.wf("\"%s\"", quotedReplacer.Replace(s))
}
func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) {
length := rv.Len()
enc.wf("[")
for i := 0; i < length; i++ {
elem := rv.Index(i)
enc.eElement(elem)
if i != length-1 {
enc.wf(", ")
}
}
enc.wf("]")
}
func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
if len(key) == 0 {
encPanic(errNoKey)
}
panicIfInvalidKey(key, true)
for i := 0; i < rv.Len(); i++ {
trv := rv.Index(i)
if isNil(trv) {
continue
}
enc.newline()
enc.wf("%s[[%s]]", enc.indentStr(key), key.String())
enc.newline()
enc.eMapOrStruct(key, trv)
}
}
func (enc *Encoder) eTable(key Key, rv reflect.Value) {
if len(key) == 1 {
// Output an extra new line between top-level tables.
// (The newline isn't written if nothing else has been written though.)
enc.newline()
}
if len(key) > 0 {
panicIfInvalidKey(key, true)
enc.wf("%s[%s]", enc.indentStr(key), key.String())
enc.newline()
}
enc.eMapOrStruct(key, rv)
}
func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value) {
switch rv := eindirect(rv); rv.Kind() {
case reflect.Map:
enc.eMap(key, rv)
case reflect.Struct:
enc.eStruct(key, rv)
default:
panic("eTable: unhandled reflect.Value Kind: " + rv.Kind().String())
}
}
func (enc *Encoder) eMap(key Key, rv reflect.Value) {
rt := rv.Type()
if rt.Key().Kind() != reflect.String {
encPanic(errNonString)
}
// Sort keys so that we have deterministic output. And write keys directly
// underneath this key first, before writing sub-structs or sub-maps.
var mapKeysDirect, mapKeysSub []string
for _, mapKey := range rv.MapKeys() {
k := mapKey.String()
if typeIsHash(tomlTypeOfGo(rv.MapIndex(mapKey))) {
mapKeysSub = append(mapKeysSub, k)
} else {
mapKeysDirect = append(mapKeysDirect, k)
}
}
var writeMapKeys = func(mapKeys []string) {
sort.Strings(mapKeys)
for _, mapKey := range mapKeys {
mrv := rv.MapIndex(reflect.ValueOf(mapKey))
if isNil(mrv) {
// Don't write anything for nil fields.
continue
}
enc.encode(key.add(mapKey), mrv)
}
}
writeMapKeys(mapKeysDirect)
writeMapKeys(mapKeysSub)
}
func (enc *Encoder) eStruct(key Key, rv reflect.Value) {
// Write keys for fields directly under this key first, because if we write
// a field that creates a new table, then all keys under it will be in that
// table (not the one we're writing here).
rt := rv.Type()
var fieldsDirect, fieldsSub [][]int
var addFields func(rt reflect.Type, rv reflect.Value, start []int)
addFields = func(rt reflect.Type, rv reflect.Value, start []int) {
for i := 0; i < rt.NumField(); i++ {
f := rt.Field(i)
// skip unexporded fields
if f.PkgPath != "" {
continue
}
frv := rv.Field(i)
if f.Anonymous {
frv := eindirect(frv)
t := frv.Type()
if t.Kind() != reflect.Struct {
encPanic(errAnonNonStruct)
}
addFields(t, frv, f.Index)
} else if typeIsHash(tomlTypeOfGo(frv)) {
fieldsSub = append(fieldsSub, append(start, f.Index...))
} else {
fieldsDirect = append(fieldsDirect, append(start, f.Index...))
}
}
}
addFields(rt, rv, nil)
var writeFields = func(fields [][]int) {
for _, fieldIndex := range fields {
sft := rt.FieldByIndex(fieldIndex)
sf := rv.FieldByIndex(fieldIndex)
if isNil(sf) {
// Don't write anything for nil fields.
continue
}
keyName := sft.Tag.Get("toml")
if keyName == "-" {
continue
}
if keyName == "" {
keyName = sft.Name
}
enc.encode(key.add(keyName), sf)
}
}
writeFields(fieldsDirect)
writeFields(fieldsSub)
}
// tomlTypeName returns the TOML type name of the Go value's type. It is used to
// determine whether the types of array elements are mixed (which is forbidden).
// If the Go value is nil, then it is illegal for it to be an array element, and
// valueIsNil is returned as true.
// Returns the TOML type of a Go value. The type may be `nil`, which means
// no concrete TOML type could be found.
func tomlTypeOfGo(rv reflect.Value) tomlType {
if isNil(rv) || !rv.IsValid() {
return nil
}
switch rv.Kind() {
case reflect.Bool:
return tomlBool
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
reflect.Uint64:
return tomlInteger
case reflect.Float32, reflect.Float64:
return tomlFloat
case reflect.Array, reflect.Slice:
if typeEqual(tomlHash, tomlArrayType(rv)) {
return tomlArrayHash
} else {
return tomlArray
}
case reflect.Ptr, reflect.Interface:
return tomlTypeOfGo(rv.Elem())
case reflect.String:
return tomlString
case reflect.Map:
return tomlHash
case reflect.Struct:
switch rv.Interface().(type) {
case time.Time:
return tomlDatetime
case TextMarshaler:
return tomlString
default:
return tomlHash
}
default:
panic("unexpected reflect.Kind: " + rv.Kind().String())
}
}
// tomlArrayType returns the element type of a TOML array. The type returned
// may be nil if it cannot be determined (e.g., a nil slice or a zero length
// slize). This function may also panic if it finds a type that cannot be
// expressed in TOML (such as nil elements, heterogeneous arrays or directly
// nested arrays of tables).
func tomlArrayType(rv reflect.Value) tomlType {
if isNil(rv) || !rv.IsValid() || rv.Len() == 0 {
return nil
}
firstType := tomlTypeOfGo(rv.Index(0))
if firstType == nil {
encPanic(errArrayNilElement)
}
rvlen := rv.Len()
for i := 1; i < rvlen; i++ {
elem := rv.Index(i)
switch elemType := tomlTypeOfGo(elem); {
case elemType == nil:
encPanic(errArrayNilElement)
case !typeEqual(firstType, elemType):
encPanic(errArrayMixedElementTypes)
}
}
// If we have a nested array, then we must make sure that the nested
// array contains ONLY primitives.
// This checks arbitrarily nested arrays.
if typeEqual(firstType, tomlArray) || typeEqual(firstType, tomlArrayHash) {
nest := tomlArrayType(eindirect(rv.Index(0)))
if typeEqual(nest, tomlHash) || typeEqual(nest, tomlArrayHash) {
encPanic(errArrayNoTable)
}
}
return firstType
}
func (enc *Encoder) newline() {
if enc.hasWritten {
enc.wf("\n")
}
}
func (enc *Encoder) keyEqElement(key Key, val reflect.Value) {
if len(key) == 0 {
encPanic(errNoKey)
}
panicIfInvalidKey(key, false)
enc.wf("%s%s = ", enc.indentStr(key), key[len(key)-1])
enc.eElement(val)
enc.newline()
}
func (enc *Encoder) wf(format string, v ...interface{}) {
if _, err := fmt.Fprintf(enc.w, format, v...); err != nil {
encPanic(err)
}
enc.hasWritten = true
}
func (enc *Encoder) indentStr(key Key) string {
return strings.Repeat(enc.Indent, len(key)-1)
}
func encPanic(err error) {
panic(tomlEncodeError{err})
}
func eindirect(v reflect.Value) reflect.Value {
switch v.Kind() {
case reflect.Ptr, reflect.Interface:
return eindirect(v.Elem())
default:
return v
}
}
func isNil(rv reflect.Value) bool {
switch rv.Kind() {
case reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return rv.IsNil()
default:
return false
}
}
func panicIfInvalidKey(key Key, hash bool) {
if hash {
for _, k := range key {
if !isValidTableName(k) {
encPanic(e("Key '%s' is not a valid table name. Table names "+
"cannot contain '[', ']' or '.'.", key.String()))
}
}
} else {
if !isValidKeyName(key[len(key)-1]) {
encPanic(e("Key '%s' is not a name. Key names "+
"cannot contain whitespace.", key.String()))
}
}
}
func isValidTableName(s string) bool {
if len(s) == 0 {
return false
}
for _, r := range s {
if r == '[' || r == ']' || r == '.' {
return false
}
}
return true
}
func isValidKeyName(s string) bool {
if len(s) == 0 {
return false
}
return true
}
-506
View File
@@ -1,506 +0,0 @@
package toml
import (
"bytes"
"fmt"
"log"
"net"
"testing"
"time"
)
func TestEncodeRoundTrip(t *testing.T) {
type Config struct {
Age int
Cats []string
Pi float64
Perfection []int
DOB time.Time
Ipaddress net.IP
}
var inputs = Config{
13,
[]string{"one", "two", "three"},
3.145,
[]int{11, 2, 3, 4},
time.Now(),
net.ParseIP("192.168.59.254"),
}
var firstBuffer bytes.Buffer
e := NewEncoder(&firstBuffer)
err := e.Encode(inputs)
if err != nil {
t.Fatal(err)
}
var outputs Config
if _, err := Decode(firstBuffer.String(), &outputs); err != nil {
log.Printf("Could not decode:\n-----\n%s\n-----\n",
firstBuffer.String())
t.Fatal(err)
}
// could test each value individually, but I'm lazy
var secondBuffer bytes.Buffer
e2 := NewEncoder(&secondBuffer)
err = e2.Encode(outputs)
if err != nil {
t.Fatal(err)
}
if firstBuffer.String() != secondBuffer.String() {
t.Error(
firstBuffer.String(),
"\n\n is not identical to\n\n",
secondBuffer.String())
}
}
// XXX(burntsushi)
// I think these tests probably should be removed. They are good, but they
// ought to be obsolete by toml-test.
func TestEncode(t *testing.T) {
type Embedded struct {
Int int `toml:"_int"`
}
type NonStruct int
date := time.Date(2014, 5, 11, 20, 30, 40, 0, time.FixedZone("IST", 3600))
dateStr := "2014-05-11T19:30:40Z"
tests := map[string]struct {
input interface{}
wantOutput string
wantError error
}{
"bool field": {
input: struct {
BoolTrue bool
BoolFalse bool
}{true, false},
wantOutput: "BoolTrue = true\nBoolFalse = false\n",
},
"int fields": {
input: struct {
Int int
Int8 int8
Int16 int16
Int32 int32
Int64 int64
}{1, 2, 3, 4, 5},
wantOutput: "Int = 1\nInt8 = 2\nInt16 = 3\nInt32 = 4\nInt64 = 5\n",
},
"uint fields": {
input: struct {
Uint uint
Uint8 uint8
Uint16 uint16
Uint32 uint32
Uint64 uint64
}{1, 2, 3, 4, 5},
wantOutput: "Uint = 1\nUint8 = 2\nUint16 = 3\nUint32 = 4" +
"\nUint64 = 5\n",
},
"float fields": {
input: struct {
Float32 float32
Float64 float64
}{1.5, 2.5},
wantOutput: "Float32 = 1.5\nFloat64 = 2.5\n",
},
"string field": {
input: struct{ String string }{"foo"},
wantOutput: "String = \"foo\"\n",
},
"string field and unexported field": {
input: struct {
String string
unexported int
}{"foo", 0},
wantOutput: "String = \"foo\"\n",
},
"datetime field in UTC": {
input: struct{ Date time.Time }{date},
wantOutput: fmt.Sprintf("Date = %s\n", dateStr),
},
"datetime field as primitive": {
// Using a map here to fail if isStructOrMap() returns true for
// time.Time.
input: map[string]interface{}{
"Date": date,
"Int": 1,
},
wantOutput: fmt.Sprintf("Date = %s\nInt = 1\n", dateStr),
},
"array fields": {
input: struct {
IntArray0 [0]int
IntArray3 [3]int
}{[0]int{}, [3]int{1, 2, 3}},
wantOutput: "IntArray0 = []\nIntArray3 = [1, 2, 3]\n",
},
"slice fields": {
input: struct{ IntSliceNil, IntSlice0, IntSlice3 []int }{
nil, []int{}, []int{1, 2, 3},
},
wantOutput: "IntSlice0 = []\nIntSlice3 = [1, 2, 3]\n",
},
"datetime slices": {
input: struct{ DatetimeSlice []time.Time }{
[]time.Time{date, date},
},
wantOutput: fmt.Sprintf("DatetimeSlice = [%s, %s]\n",
dateStr, dateStr),
},
"nested arrays and slices": {
input: struct {
SliceOfArrays [][2]int
ArrayOfSlices [2][]int
SliceOfArraysOfSlices [][2][]int
ArrayOfSlicesOfArrays [2][][2]int
SliceOfMixedArrays [][2]interface{}
ArrayOfMixedSlices [2][]interface{}
}{
[][2]int{{1, 2}, {3, 4}},
[2][]int{{1, 2}, {3, 4}},
[][2][]int{
{
{1, 2}, {3, 4},
},
{
{5, 6}, {7, 8},
},
},
[2][][2]int{
{
{1, 2}, {3, 4},
},
{
{5, 6}, {7, 8},
},
},
[][2]interface{}{
{1, 2}, {"a", "b"},
},
[2][]interface{}{
{1, 2}, {"a", "b"},
},
},
wantOutput: `SliceOfArrays = [[1, 2], [3, 4]]
ArrayOfSlices = [[1, 2], [3, 4]]
SliceOfArraysOfSlices = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
ArrayOfSlicesOfArrays = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
SliceOfMixedArrays = [[1, 2], ["a", "b"]]
ArrayOfMixedSlices = [[1, 2], ["a", "b"]]
`,
},
"empty slice": {
input: struct{ Empty []interface{} }{[]interface{}{}},
wantOutput: "Empty = []\n",
},
"(error) slice with element type mismatch (string and integer)": {
input: struct{ Mixed []interface{} }{[]interface{}{1, "a"}},
wantError: errArrayMixedElementTypes,
},
"(error) slice with element type mismatch (integer and float)": {
input: struct{ Mixed []interface{} }{[]interface{}{1, 2.5}},
wantError: errArrayMixedElementTypes,
},
"slice with elems of differing Go types, same TOML types": {
input: struct {
MixedInts []interface{}
MixedFloats []interface{}
}{
[]interface{}{
int(1), int8(2), int16(3), int32(4), int64(5),
uint(1), uint8(2), uint16(3), uint32(4), uint64(5),
},
[]interface{}{float32(1.5), float64(2.5)},
},
wantOutput: "MixedInts = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]\n" +
"MixedFloats = [1.5, 2.5]\n",
},
"(error) slice w/ element type mismatch (one is nested array)": {
input: struct{ Mixed []interface{} }{
[]interface{}{1, []interface{}{2}},
},
wantError: errArrayMixedElementTypes,
},
"(error) slice with 1 nil element": {
input: struct{ NilElement1 []interface{} }{[]interface{}{nil}},
wantError: errArrayNilElement,
},
"(error) slice with 1 nil element (and other non-nil elements)": {
input: struct{ NilElement []interface{} }{
[]interface{}{1, nil},
},
wantError: errArrayNilElement,
},
"simple map": {
input: map[string]int{"a": 1, "b": 2},
wantOutput: "a = 1\nb = 2\n",
},
"map with interface{} value type": {
input: map[string]interface{}{"a": 1, "b": "c"},
wantOutput: "a = 1\nb = \"c\"\n",
},
"map with interface{} value type, some of which are structs": {
input: map[string]interface{}{
"a": struct{ Int int }{2},
"b": 1,
},
wantOutput: "b = 1\n\n[a]\n Int = 2\n",
},
"nested map": {
input: map[string]map[string]int{
"a": {"b": 1},
"c": {"d": 2},
},
wantOutput: "[a]\n b = 1\n\n[c]\n d = 2\n",
},
"nested struct": {
input: struct{ Struct struct{ Int int } }{
struct{ Int int }{1},
},
wantOutput: "[Struct]\n Int = 1\n",
},
"nested struct and non-struct field": {
input: struct {
Struct struct{ Int int }
Bool bool
}{struct{ Int int }{1}, true},
wantOutput: "Bool = true\n\n[Struct]\n Int = 1\n",
},
"2 nested structs": {
input: struct{ Struct1, Struct2 struct{ Int int } }{
struct{ Int int }{1}, struct{ Int int }{2},
},
wantOutput: "[Struct1]\n Int = 1\n\n[Struct2]\n Int = 2\n",
},
"deeply nested structs": {
input: struct {
Struct1, Struct2 struct{ Struct3 *struct{ Int int } }
}{
struct{ Struct3 *struct{ Int int } }{&struct{ Int int }{1}},
struct{ Struct3 *struct{ Int int } }{nil},
},
wantOutput: "[Struct1]\n [Struct1.Struct3]\n Int = 1" +
"\n\n[Struct2]\n",
},
"nested struct with nil struct elem": {
input: struct {
Struct struct{ Inner *struct{ Int int } }
}{
struct{ Inner *struct{ Int int } }{nil},
},
wantOutput: "[Struct]\n",
},
"nested struct with no fields": {
input: struct {
Struct struct{ Inner struct{} }
}{
struct{ Inner struct{} }{struct{}{}},
},
wantOutput: "[Struct]\n [Struct.Inner]\n",
},
"struct with tags": {
input: struct {
Struct struct {
Int int `toml:"_int"`
} `toml:"_struct"`
Bool bool `toml:"_bool"`
}{
struct {
Int int `toml:"_int"`
}{1}, true,
},
wantOutput: "_bool = true\n\n[_struct]\n _int = 1\n",
},
"embedded struct": {
input: struct{ Embedded }{Embedded{1}},
wantOutput: "_int = 1\n",
},
"embedded *struct": {
input: struct{ *Embedded }{&Embedded{1}},
wantOutput: "_int = 1\n",
},
"nested embedded struct": {
input: struct {
Struct struct{ Embedded } `toml:"_struct"`
}{struct{ Embedded }{Embedded{1}}},
wantOutput: "[_struct]\n _int = 1\n",
},
"nested embedded *struct": {
input: struct {
Struct struct{ *Embedded } `toml:"_struct"`
}{struct{ *Embedded }{&Embedded{1}}},
wantOutput: "[_struct]\n _int = 1\n",
},
"array of tables": {
input: struct {
Structs []*struct{ Int int } `toml:"struct"`
}{
[]*struct{ Int int }{{1}, {3}},
},
wantOutput: "[[struct]]\n Int = 1\n\n[[struct]]\n Int = 3\n",
},
"array of tables order": {
input: map[string]interface{}{
"map": map[string]interface{}{
"zero": 5,
"arr": []map[string]int{
map[string]int{
"friend": 5,
},
},
},
},
wantOutput: "[map]\n zero = 5\n\n [[map.arr]]\n friend = 5\n",
},
"(error) top-level slice": {
input: []struct{ Int int }{{1}, {2}, {3}},
wantError: errNoKey,
},
"(error) slice of slice": {
input: struct {
Slices [][]struct{ Int int }
}{
[][]struct{ Int int }{{{1}}, {{2}}, {{3}}},
},
wantError: errArrayNoTable,
},
"(error) map no string key": {
input: map[int]string{1: ""},
wantError: errNonString,
},
"(error) anonymous non-struct": {
input: struct{ NonStruct }{5},
wantError: errAnonNonStruct,
},
"(error) empty key name": {
input: map[string]int{"": 1},
wantError: errAnything,
},
"(error) empty map name": {
input: map[string]interface{}{
"": map[string]int{"v": 1},
},
wantError: errAnything,
},
}
for label, test := range tests {
encodeExpected(t, label, test.input, test.wantOutput, test.wantError)
}
}
func TestEncodeNestedTableArrays(t *testing.T) {
type song struct {
Name string `toml:"name"`
}
type album struct {
Name string `toml:"name"`
Songs []song `toml:"songs"`
}
type springsteen struct {
Albums []album `toml:"albums"`
}
value := springsteen{
[]album{
{"Born to Run",
[]song{{"Jungleland"}, {"Meeting Across the River"}}},
{"Born in the USA",
[]song{{"Glory Days"}, {"Dancing in the Dark"}}},
},
}
expected := `[[albums]]
name = "Born to Run"
[[albums.songs]]
name = "Jungleland"
[[albums.songs]]
name = "Meeting Across the River"
[[albums]]
name = "Born in the USA"
[[albums.songs]]
name = "Glory Days"
[[albums.songs]]
name = "Dancing in the Dark"
`
encodeExpected(t, "nested table arrays", value, expected, nil)
}
func TestEncodeArrayHashWithNormalHashOrder(t *testing.T) {
type Alpha struct {
V int
}
type Beta struct {
V int
}
type Conf struct {
V int
A Alpha
B []Beta
}
val := Conf{
V: 1,
A: Alpha{2},
B: []Beta{{3}},
}
expected := "V = 1\n\n[A]\n V = 2\n\n[[B]]\n V = 3\n"
encodeExpected(t, "array hash with normal hash order", val, expected, nil)
}
func encodeExpected(
t *testing.T, label string, val interface{}, wantStr string, wantErr error,
) {
var buf bytes.Buffer
enc := NewEncoder(&buf)
err := enc.Encode(val)
if err != wantErr {
if wantErr != nil {
if wantErr == errAnything && err != nil {
return
}
t.Errorf("%s: want Encode error %v, got %v", label, wantErr, err)
} else {
t.Errorf("%s: Encode failed: %s", label, err)
}
}
if err != nil {
return
}
if got := buf.String(); wantStr != got {
t.Errorf("%s: want\n-----\n%q\n-----\nbut got\n-----\n%q\n-----\n",
label, wantStr, got)
}
}
func ExampleEncoder_Encode() {
date, _ := time.Parse(time.RFC822, "14 Mar 10 18:00 UTC")
var config = map[string]interface{}{
"date": date,
"counts": []int{1, 1, 2, 3, 5, 8},
"hash": map[string]string{
"key1": "val1",
"key2": "val2",
},
}
buf := new(bytes.Buffer)
if err := NewEncoder(buf).Encode(config); err != nil {
log.Fatal(err)
}
fmt.Println(buf.String())
// Output:
// counts = [1, 1, 2, 3, 5, 8]
// date = 2010-03-14T18:00:00Z
//
// [hash]
// key1 = "val1"
// key2 = "val2"
}
-19
View File
@@ -1,19 +0,0 @@
// +build go1.2
package toml
// In order to support Go 1.1, we define our own TextMarshaler and
// TextUnmarshaler types. For Go 1.2+, we just alias them with the
// standard library interfaces.
import (
"encoding"
)
// TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here
// so that Go 1.1 can be supported.
type TextMarshaler encoding.TextMarshaler
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined here
// so that Go 1.1 can be supported.
type TextUnmarshaler encoding.TextUnmarshaler
-18
View File
@@ -1,18 +0,0 @@
// +build !go1.2
package toml
// These interfaces were introduced in Go 1.2, so we add them manually when
// compiling for Go 1.1.
// TextMarshaler is a synonym for encoding.TextMarshaler. It is defined here
// so that Go 1.1 can be supported.
type TextMarshaler interface {
MarshalText() (text []byte, err error)
}
// TextUnmarshaler is a synonym for encoding.TextUnmarshaler. It is defined here
// so that Go 1.1 can be supported.
type TextUnmarshaler interface {
UnmarshalText(text []byte) error
}
-863
View File
@@ -1,863 +0,0 @@
package toml
import (
"fmt"
"strings"
"unicode/utf8"
)
type itemType int
const (
itemError itemType = iota
itemNIL // used in the parser to indicate no type
itemEOF
itemText
itemString
itemRawString
itemMultilineString
itemRawMultilineString
itemBool
itemInteger
itemFloat
itemDatetime
itemArray // the start of an array
itemArrayEnd
itemTableStart
itemTableEnd
itemArrayTableStart
itemArrayTableEnd
itemKeyStart
itemCommentStart
)
const (
eof = 0
tableStart = '['
tableEnd = ']'
arrayTableStart = '['
arrayTableEnd = ']'
tableSep = '.'
keySep = '='
arrayStart = '['
arrayEnd = ']'
arrayValTerm = ','
commentStart = '#'
stringStart = '"'
stringEnd = '"'
rawStringStart = '\''
rawStringEnd = '\''
)
type stateFn func(lx *lexer) stateFn
type lexer struct {
input string
start int
pos int
width int
line int
state stateFn
items chan item
// A stack of state functions used to maintain context.
// The idea is to reuse parts of the state machine in various places.
// For example, values can appear at the top level or within arbitrarily
// nested arrays. The last state on the stack is used after a value has
// been lexed. Similarly for comments.
stack []stateFn
}
type item struct {
typ itemType
val string
line int
}
func (lx *lexer) nextItem() item {
for {
select {
case item := <-lx.items:
return item
default:
lx.state = lx.state(lx)
}
}
}
func lex(input string) *lexer {
lx := &lexer{
input: input + "\n",
state: lexTop,
line: 1,
items: make(chan item, 10),
stack: make([]stateFn, 0, 10),
}
return lx
}
func (lx *lexer) push(state stateFn) {
lx.stack = append(lx.stack, state)
}
func (lx *lexer) pop() stateFn {
if len(lx.stack) == 0 {
return lx.errorf("BUG in lexer: no states to pop.")
}
last := lx.stack[len(lx.stack)-1]
lx.stack = lx.stack[0 : len(lx.stack)-1]
return last
}
func (lx *lexer) current() string {
return lx.input[lx.start:lx.pos]
}
func (lx *lexer) emit(typ itemType) {
lx.items <- item{typ, lx.current(), lx.line}
lx.start = lx.pos
}
func (lx *lexer) emitTrim(typ itemType) {
lx.items <- item{typ, strings.TrimSpace(lx.current()), lx.line}
lx.start = lx.pos
}
func (lx *lexer) next() (r rune) {
if lx.pos >= len(lx.input) {
lx.width = 0
return eof
}
if lx.input[lx.pos] == '\n' {
lx.line++
}
r, lx.width = utf8.DecodeRuneInString(lx.input[lx.pos:])
lx.pos += lx.width
return r
}
// ignore skips over the pending input before this point.
func (lx *lexer) ignore() {
lx.start = lx.pos
}
// backup steps back one rune. Can be called only once per call of next.
func (lx *lexer) backup() {
lx.pos -= lx.width
if lx.pos < len(lx.input) && lx.input[lx.pos] == '\n' {
lx.line--
}
}
// accept consumes the next rune if it's equal to `valid`.
func (lx *lexer) accept(valid rune) bool {
if lx.next() == valid {
return true
}
lx.backup()
return false
}
// peek returns but does not consume the next rune in the input.
func (lx *lexer) peek() rune {
r := lx.next()
lx.backup()
return r
}
// errorf stops all lexing by emitting an error and returning `nil`.
// Note that any value that is a character is escaped if it's a special
// character (new lines, tabs, etc.).
func (lx *lexer) errorf(format string, values ...interface{}) stateFn {
lx.items <- item{
itemError,
fmt.Sprintf(format, values...),
lx.line,
}
return nil
}
// lexTop consumes elements at the top level of TOML data.
func lexTop(lx *lexer) stateFn {
r := lx.next()
if isWhitespace(r) || isNL(r) {
return lexSkip(lx, lexTop)
}
switch r {
case commentStart:
lx.push(lexTop)
return lexCommentStart
case tableStart:
return lexTableStart
case eof:
if lx.pos > lx.start {
return lx.errorf("Unexpected EOF.")
}
lx.emit(itemEOF)
return nil
}
// At this point, the only valid item can be a key, so we back up
// and let the key lexer do the rest.
lx.backup()
lx.push(lexTopEnd)
return lexKeyStart
}
// lexTopEnd is entered whenever a top-level item has been consumed. (A value
// or a table.) It must see only whitespace, and will turn back to lexTop
// upon a new line. If it sees EOF, it will quit the lexer successfully.
func lexTopEnd(lx *lexer) stateFn {
r := lx.next()
switch {
case r == commentStart:
// a comment will read to a new line for us.
lx.push(lexTop)
return lexCommentStart
case isWhitespace(r):
return lexTopEnd
case isNL(r):
lx.ignore()
return lexTop
case r == eof:
lx.ignore()
return lexTop
}
return lx.errorf("Expected a top-level item to end with a new line, "+
"comment or EOF, but got %q instead.", r)
}
// lexTable lexes the beginning of a table. Namely, it makes sure that
// it starts with a character other than '.' and ']'.
// It assumes that '[' has already been consumed.
// It also handles the case that this is an item in an array of tables.
// e.g., '[[name]]'.
func lexTableStart(lx *lexer) stateFn {
if lx.peek() == arrayTableStart {
lx.next()
lx.emit(itemArrayTableStart)
lx.push(lexArrayTableEnd)
} else {
lx.emit(itemTableStart)
lx.push(lexTableEnd)
}
return lexTableNameStart
}
func lexTableEnd(lx *lexer) stateFn {
lx.emit(itemTableEnd)
return lexTopEnd
}
func lexArrayTableEnd(lx *lexer) stateFn {
if r := lx.next(); r != arrayTableEnd {
return lx.errorf("Expected end of table array name delimiter %q, "+
"but got %q instead.", arrayTableEnd, r)
}
lx.emit(itemArrayTableEnd)
return lexTopEnd
}
func lexTableNameStart(lx *lexer) stateFn {
switch lx.next() {
case tableEnd, eof:
return lx.errorf("Unexpected end of table. (Tables cannot " +
"be empty.)")
case tableSep:
return lx.errorf("Unexpected table separator. (Tables cannot " +
"be empty.)")
}
return lexTableName
}
// lexTableName lexes the name of a table. It assumes that at least one
// valid character for the table has already been read.
func lexTableName(lx *lexer) stateFn {
switch lx.peek() {
case eof:
return lx.errorf("Unexpected end of table name %q.", lx.current())
case tableStart:
return lx.errorf("Table names cannot contain %q or %q.",
tableStart, tableEnd)
case tableEnd:
lx.emit(itemText)
lx.next()
return lx.pop()
case tableSep:
lx.emit(itemText)
lx.next()
lx.ignore()
return lexTableNameStart
}
lx.next()
return lexTableName
}
// lexKeyStart consumes a key name up until the first non-whitespace character.
// lexKeyStart will ignore whitespace.
func lexKeyStart(lx *lexer) stateFn {
r := lx.peek()
switch {
case r == keySep:
return lx.errorf("Unexpected key separator %q.", keySep)
case isWhitespace(r) || isNL(r):
lx.next()
return lexSkip(lx, lexKeyStart)
}
lx.ignore()
lx.emit(itemKeyStart)
lx.next()
return lexKey
}
// lexKey consumes the text of a key. Assumes that the first character (which
// is not whitespace) has already been consumed.
func lexKey(lx *lexer) stateFn {
r := lx.peek()
// Keys cannot contain a '#' character.
if r == commentStart {
return lx.errorf("Key cannot contain a '#' character.")
}
// XXX: Possible divergence from spec?
// "Keys start with the first non-whitespace character and end with the
// last non-whitespace character before the equals sign."
// Note here that whitespace is either a tab or a space.
// But we'll call it quits if we see a new line too.
if isNL(r) {
lx.emitTrim(itemText)
return lexKeyEnd
}
// Let's also call it quits if we see an equals sign.
if r == keySep {
lx.emitTrim(itemText)
return lexKeyEnd
}
lx.next()
return lexKey
}
// lexKeyEnd consumes the end of a key (up to the key separator).
// Assumes that any whitespace after a key has been consumed.
func lexKeyEnd(lx *lexer) stateFn {
r := lx.next()
if r == keySep {
return lexSkip(lx, lexValue)
}
return lx.errorf("Expected key separator %q, but got %q instead.",
keySep, r)
}
// lexValue starts the consumption of a value anywhere a value is expected.
// lexValue will ignore whitespace.
// After a value is lexed, the last state on the next is popped and returned.
func lexValue(lx *lexer) stateFn {
// We allow whitespace to precede a value, but NOT new lines.
// In array syntax, the array states are responsible for ignoring new lines.
r := lx.next()
if isWhitespace(r) {
return lexSkip(lx, lexValue)
}
switch {
case r == arrayStart:
lx.ignore()
lx.emit(itemArray)
return lexArrayValue
case r == stringStart:
if lx.accept(stringStart) {
if lx.accept(stringStart) {
lx.ignore() // Ignore """
return lexMultilineString
}
lx.backup()
}
lx.ignore() // ignore the '"'
return lexString
case r == rawStringStart:
if lx.accept(rawStringStart) {
if lx.accept(rawStringStart) {
lx.ignore() // Ignore """
return lexMultilineRawString
}
lx.backup()
}
lx.ignore() // ignore the "'"
return lexRawString
case r == 't':
return lexTrue
case r == 'f':
return lexFalse
case r == '-':
return lexNumberStart
case isDigit(r):
lx.backup() // avoid an extra state and use the same as above
return lexNumberOrDateStart
case r == '.': // special error case, be kind to users
return lx.errorf("Floats must start with a digit, not '.'.")
}
return lx.errorf("Expected value but found %q instead.", r)
}
// lexArrayValue consumes one value in an array. It assumes that '[' or ','
// have already been consumed. All whitespace and new lines are ignored.
func lexArrayValue(lx *lexer) stateFn {
r := lx.next()
switch {
case isWhitespace(r) || isNL(r):
return lexSkip(lx, lexArrayValue)
case r == commentStart:
lx.push(lexArrayValue)
return lexCommentStart
case r == arrayValTerm:
return lx.errorf("Unexpected array value terminator %q.",
arrayValTerm)
case r == arrayEnd:
return lexArrayEnd
}
lx.backup()
lx.push(lexArrayValueEnd)
return lexValue
}
// lexArrayValueEnd consumes the cruft between values of an array. Namely,
// it ignores whitespace and expects either a ',' or a ']'.
func lexArrayValueEnd(lx *lexer) stateFn {
r := lx.next()
switch {
case isWhitespace(r) || isNL(r):
return lexSkip(lx, lexArrayValueEnd)
case r == commentStart:
lx.push(lexArrayValueEnd)
return lexCommentStart
case r == arrayValTerm:
lx.ignore()
return lexArrayValue // move on to the next value
case r == arrayEnd:
return lexArrayEnd
}
return lx.errorf("Expected an array value terminator %q or an array "+
"terminator %q, but got %q instead.", arrayValTerm, arrayEnd, r)
}
// lexArrayEnd finishes the lexing of an array. It assumes that a ']' has
// just been consumed.
func lexArrayEnd(lx *lexer) stateFn {
lx.ignore()
lx.emit(itemArrayEnd)
return lx.pop()
}
// lexString consumes the inner contents of a string. It assumes that the
// beginning '"' has already been consumed and ignored.
func lexString(lx *lexer) stateFn {
r := lx.next()
switch {
case isNL(r):
return lx.errorf("Strings cannot contain new lines.")
case r == '\\':
return lexStringEscape
case r == stringEnd:
lx.backup()
lx.emit(itemString)
lx.next()
lx.ignore()
return lx.pop()
}
return lexString
}
// lexStringEscape consumes an escaped character. It assumes that the preceding
// '\\' has already been consumed.
func lexStringEscape(lx *lexer) stateFn {
return lexStringEscapeHandler(lx, lexString, lexStringUnicode)
}
// lexMultilineStringEscape consumes an escaped character. It assumes that the
// preceding '\\' has already been consumed.
func lexMultilineStringEscape(lx *lexer) stateFn {
// Handle the special case first:
if isNL(lx.next()) {
lx.next()
return lexMultilineString
} else {
lx.backup()
return lexStringEscapeHandler(lx, lexMultilineString, lexMultilineStringUnicode)
}
}
func lexStringEscapeHandler(lx *lexer, stringFn stateFn, unicodeFn stateFn) stateFn {
r := lx.next()
switch r {
case 'b':
fallthrough
case 't':
fallthrough
case 'n':
fallthrough
case 'f':
fallthrough
case 'r':
fallthrough
case '"':
fallthrough
case '/':
fallthrough
case '\\':
return stringFn
case 'u':
return unicodeFn
}
return lx.errorf("Invalid escape character %q. Only the following "+
"escape characters are allowed: "+
"\\b, \\t, \\n, \\f, \\r, \\\", \\/, \\\\, and \\uXXXX.", r)
}
// lexStringUnicode consumes four hexadecimal digits following '\u'. It assumes
// that the '\u' has already been consumed.
func lexStringUnicode(lx *lexer) stateFn {
return lexStringUnicodeHandler(lx, lexString)
}
// lexMultilineStringUnicode consumes four hexadecimal digits following '\u'.
// It assumes that the '\u' has already been consumed.
func lexMultilineStringUnicode(lx *lexer) stateFn {
return lexStringUnicodeHandler(lx, lexMultilineString)
}
func lexStringUnicodeHandler(lx *lexer, nextFunc stateFn) stateFn {
var r rune
for i := 0; i < 4; i++ {
r = lx.next()
if !isHexadecimal(r) {
return lx.errorf("Expected four hexadecimal digits after '\\x', "+
"but got '%s' instead.", lx.current())
}
}
return nextFunc
}
// lexMultilineString consumes the inner contents of a string. It assumes that
// the beginning '"""' has already been consumed and ignored.
func lexMultilineString(lx *lexer) stateFn {
r := lx.next()
switch {
case r == '\\':
return lexMultilineStringEscape
case r == stringEnd:
if lx.accept(stringEnd) {
if lx.accept(stringEnd) {
lx.backup()
lx.backup()
lx.backup()
lx.emit(itemMultilineString)
lx.next()
lx.next()
lx.next()
lx.ignore()
return lx.pop()
}
lx.backup()
}
}
return lexMultilineString
}
// lexRawString consumes a raw string. Nothing can be escaped in such a string.
// It assumes that the beginning "'" has already been consumed and ignored.
func lexRawString(lx *lexer) stateFn {
r := lx.next()
switch {
case isNL(r):
return lx.errorf("Strings cannot contain new lines.")
case r == rawStringEnd:
lx.backup()
lx.emit(itemRawString)
lx.next()
lx.ignore()
return lx.pop()
}
return lexRawString
}
// lexMultilineRawString consumes a raw string. Nothing can be escaped in such
// a string. It assumes that the beginning "'" has already been consumed and
// ignored.
func lexMultilineRawString(lx *lexer) stateFn {
r := lx.next()
switch {
case r == rawStringEnd:
if lx.accept(rawStringEnd) {
if lx.accept(rawStringEnd) {
lx.backup()
lx.backup()
lx.backup()
lx.emit(itemRawMultilineString)
lx.next()
lx.next()
lx.next()
lx.ignore()
return lx.pop()
}
lx.backup()
}
}
return lexMultilineRawString
}
// lexNumberOrDateStart consumes either a (positive) integer, float or datetime.
// It assumes that NO negative sign has been consumed.
func lexNumberOrDateStart(lx *lexer) stateFn {
r := lx.next()
if !isDigit(r) {
if r == '.' {
return lx.errorf("Floats must start with a digit, not '.'.")
} else {
return lx.errorf("Expected a digit but got %q.", r)
}
}
return lexNumberOrDate
}
// lexNumberOrDate consumes either a (positive) integer, float or datetime.
func lexNumberOrDate(lx *lexer) stateFn {
r := lx.next()
switch {
case r == '-':
if lx.pos-lx.start != 5 {
return lx.errorf("All ISO8601 dates must be in full Zulu form.")
}
return lexDateAfterYear
case isDigit(r):
return lexNumberOrDate
case r == '.':
return lexFloatStart
}
lx.backup()
lx.emit(itemInteger)
return lx.pop()
}
// lexDateAfterYear consumes a full Zulu Datetime in ISO8601 format.
// It assumes that "YYYY-" has already been consumed.
func lexDateAfterYear(lx *lexer) stateFn {
formats := []rune{
// digits are '0'.
// everything else is direct equality.
'0', '0', '-', '0', '0',
'T',
'0', '0', ':', '0', '0', ':', '0', '0',
'Z',
}
for _, f := range formats {
r := lx.next()
if f == '0' {
if !isDigit(r) {
return lx.errorf("Expected digit in ISO8601 datetime, "+
"but found %q instead.", r)
}
} else if f != r {
return lx.errorf("Expected %q in ISO8601 datetime, "+
"but found %q instead.", f, r)
}
}
lx.emit(itemDatetime)
return lx.pop()
}
// lexNumberStart consumes either an integer or a float. It assumes that a
// negative sign has already been read, but that *no* digits have been consumed.
// lexNumberStart will move to the appropriate integer or float states.
func lexNumberStart(lx *lexer) stateFn {
// we MUST see a digit. Even floats have to start with a digit.
r := lx.next()
if !isDigit(r) {
if r == '.' {
return lx.errorf("Floats must start with a digit, not '.'.")
} else {
return lx.errorf("Expected a digit but got %q.", r)
}
}
return lexNumber
}
// lexNumber consumes an integer or a float after seeing the first digit.
func lexNumber(lx *lexer) stateFn {
r := lx.next()
switch {
case isDigit(r):
return lexNumber
case r == '.':
return lexFloatStart
}
lx.backup()
lx.emit(itemInteger)
return lx.pop()
}
// lexFloatStart starts the consumption of digits of a float after a '.'.
// Namely, at least one digit is required.
func lexFloatStart(lx *lexer) stateFn {
r := lx.next()
if !isDigit(r) {
return lx.errorf("Floats must have a digit after the '.', but got "+
"%q instead.", r)
}
return lexFloat
}
// lexFloat consumes the digits of a float after a '.'.
// Assumes that one digit has been consumed after a '.' already.
func lexFloat(lx *lexer) stateFn {
r := lx.next()
if isDigit(r) {
return lexFloat
}
lx.backup()
lx.emit(itemFloat)
return lx.pop()
}
// lexConst consumes the s[1:] in s. It assumes that s[0] has already been
// consumed.
func lexConst(lx *lexer, s string) stateFn {
for i := range s[1:] {
if r := lx.next(); r != rune(s[i+1]) {
return lx.errorf("Expected %q, but found %q instead.", s[:i+1],
s[:i]+string(r))
}
}
return nil
}
// lexTrue consumes the "rue" in "true". It assumes that 't' has already
// been consumed.
func lexTrue(lx *lexer) stateFn {
if fn := lexConst(lx, "true"); fn != nil {
return fn
}
lx.emit(itemBool)
return lx.pop()
}
// lexFalse consumes the "alse" in "false". It assumes that 'f' has already
// been consumed.
func lexFalse(lx *lexer) stateFn {
if fn := lexConst(lx, "false"); fn != nil {
return fn
}
lx.emit(itemBool)
return lx.pop()
}
// lexCommentStart begins the lexing of a comment. It will emit
// itemCommentStart and consume no characters, passing control to lexComment.
func lexCommentStart(lx *lexer) stateFn {
lx.ignore()
lx.emit(itemCommentStart)
return lexComment
}
// lexComment lexes an entire comment. It assumes that '#' has been consumed.
// It will consume *up to* the first new line character, and pass control
// back to the last state on the stack.
func lexComment(lx *lexer) stateFn {
r := lx.peek()
if isNL(r) || r == eof {
lx.emit(itemText)
return lx.pop()
}
lx.next()
return lexComment
}
// lexSkip ignores all slurped input and moves on to the next state.
func lexSkip(lx *lexer, nextState stateFn) stateFn {
return func(lx *lexer) stateFn {
lx.ignore()
return nextState
}
}
// isWhitespace returns true if `r` is a whitespace character according
// to the spec.
func isWhitespace(r rune) bool {
return r == '\t' || r == ' '
}
func isNL(r rune) bool {
return r == '\n' || r == '\r'
}
func isDigit(r rune) bool {
return r >= '0' && r <= '9'
}
func isHexadecimal(r rune) bool {
return (r >= '0' && r <= '9') ||
(r >= 'a' && r <= 'f') ||
(r >= 'A' && r <= 'F')
}
func (itype itemType) String() string {
switch itype {
case itemError:
return "Error"
case itemNIL:
return "NIL"
case itemEOF:
return "EOF"
case itemText:
return "Text"
case itemString:
return "String"
case itemRawString:
return "String"
case itemMultilineString:
return "String"
case itemRawMultilineString:
return "String"
case itemBool:
return "Bool"
case itemInteger:
return "Integer"
case itemFloat:
return "Float"
case itemDatetime:
return "DateTime"
case itemTableStart:
return "TableStart"
case itemTableEnd:
return "TableEnd"
case itemKeyStart:
return "KeyStart"
case itemArray:
return "Array"
case itemArrayEnd:
return "ArrayEnd"
case itemCommentStart:
return "CommentStart"
}
panic(fmt.Sprintf("BUG: Unknown type '%d'.", int(itype)))
}
func (item item) String() string {
return fmt.Sprintf("(%s, %s)", item.typ.String(), item.val)
}
-444
View File
@@ -1,444 +0,0 @@
package toml
import (
"fmt"
"log"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
)
type parser struct {
mapping map[string]interface{}
types map[string]tomlType
lx *lexer
// A list of keys in the order that they appear in the TOML data.
ordered []Key
// the full key for the current hash in scope
context Key
// the base key name for everything except hashes
currentKey string
// rough approximation of line number
approxLine int
// A map of 'key.group.names' to whether they were created implicitly.
implicits map[string]bool
}
type parseError string
func (pe parseError) Error() string {
return string(pe)
}
func parse(data string) (p *parser, err error) {
defer func() {
if r := recover(); r != nil {
var ok bool
if err, ok = r.(parseError); ok {
return
}
panic(r)
}
}()
p = &parser{
mapping: make(map[string]interface{}),
types: make(map[string]tomlType),
lx: lex(data),
ordered: make([]Key, 0),
implicits: make(map[string]bool),
}
for {
item := p.next()
if item.typ == itemEOF {
break
}
p.topLevel(item)
}
return p, nil
}
func (p *parser) panicf(format string, v ...interface{}) {
msg := fmt.Sprintf("Near line %d, key '%s': %s",
p.approxLine, p.current(), fmt.Sprintf(format, v...))
panic(parseError(msg))
}
func (p *parser) next() item {
it := p.lx.nextItem()
if it.typ == itemError {
p.panicf("Near line %d: %s", it.line, it.val)
}
return it
}
func (p *parser) bug(format string, v ...interface{}) {
log.Fatalf("BUG: %s\n\n", fmt.Sprintf(format, v...))
}
func (p *parser) expect(typ itemType) item {
it := p.next()
p.assertEqual(typ, it.typ)
return it
}
func (p *parser) assertEqual(expected, got itemType) {
if expected != got {
p.bug("Expected '%s' but got '%s'.", expected, got)
}
}
func (p *parser) topLevel(item item) {
switch item.typ {
case itemCommentStart:
p.approxLine = item.line
p.expect(itemText)
case itemTableStart:
kg := p.expect(itemText)
p.approxLine = kg.line
key := make(Key, 0)
for ; kg.typ == itemText; kg = p.next() {
key = append(key, kg.val)
}
p.assertEqual(itemTableEnd, kg.typ)
p.establishContext(key, false)
p.setType("", tomlHash)
p.ordered = append(p.ordered, key)
case itemArrayTableStart:
kg := p.expect(itemText)
p.approxLine = kg.line
key := make(Key, 0)
for ; kg.typ == itemText; kg = p.next() {
key = append(key, kg.val)
}
p.assertEqual(itemArrayTableEnd, kg.typ)
p.establishContext(key, true)
p.setType("", tomlArrayHash)
p.ordered = append(p.ordered, key)
case itemKeyStart:
kname := p.expect(itemText)
p.currentKey = kname.val
p.approxLine = kname.line
val, typ := p.value(p.next())
p.setValue(p.currentKey, val)
p.setType(p.currentKey, typ)
p.ordered = append(p.ordered, p.context.add(p.currentKey))
p.currentKey = ""
default:
p.bug("Unexpected type at top level: %s", item.typ)
}
}
// value translates an expected value from the lexer into a Go value wrapped
// as an empty interface.
func (p *parser) value(it item) (interface{}, tomlType) {
switch it.typ {
case itemString:
return p.replaceUnicode(replaceEscapes(it.val)), p.typeOfPrimitive(it)
case itemMultilineString:
return p.replaceUnicode(replaceEscapes(stripFirstNewline(stripEscapedWhitespace(it.val)))), p.typeOfPrimitive(it)
case itemRawString:
return it.val, p.typeOfPrimitive(it)
case itemRawMultilineString:
return stripFirstNewline(it.val), p.typeOfPrimitive(it)
case itemBool:
switch it.val {
case "true":
return true, p.typeOfPrimitive(it)
case "false":
return false, p.typeOfPrimitive(it)
}
p.bug("Expected boolean value, but got '%s'.", it.val)
case itemInteger:
num, err := strconv.ParseInt(it.val, 10, 64)
if err != nil {
// See comment below for floats describing why we make a
// distinction between a bug and a user error.
if e, ok := err.(*strconv.NumError); ok &&
e.Err == strconv.ErrRange {
p.panicf("Integer '%s' is out of the range of 64-bit "+
"signed integers.", it.val)
} else {
p.bug("Expected integer value, but got '%s'.", it.val)
}
}
return num, p.typeOfPrimitive(it)
case itemFloat:
num, err := strconv.ParseFloat(it.val, 64)
if err != nil {
// Distinguish float values. Normally, it'd be a bug if the lexer
// provides an invalid float, but it's possible that the float is
// out of range of valid values (which the lexer cannot determine).
// So mark the former as a bug but the latter as a legitimate user
// error.
//
// This is also true for integers.
if e, ok := err.(*strconv.NumError); ok &&
e.Err == strconv.ErrRange {
p.panicf("Float '%s' is out of the range of 64-bit "+
"IEEE-754 floating-point numbers.", it.val)
} else {
p.bug("Expected float value, but got '%s'.", it.val)
}
}
return num, p.typeOfPrimitive(it)
case itemDatetime:
t, err := time.Parse("2006-01-02T15:04:05Z", it.val)
if err != nil {
p.bug("Expected Zulu formatted DateTime, but got '%s'.", it.val)
}
return t, p.typeOfPrimitive(it)
case itemArray:
array := make([]interface{}, 0)
types := make([]tomlType, 0)
for it = p.next(); it.typ != itemArrayEnd; it = p.next() {
if it.typ == itemCommentStart {
p.expect(itemText)
continue
}
val, typ := p.value(it)
array = append(array, val)
types = append(types, typ)
}
return array, p.typeOfArray(types)
}
p.bug("Unexpected value type: %s", it.typ)
panic("unreachable")
}
// establishContext sets the current context of the parser,
// where the context is either a hash or an array of hashes. Which one is
// set depends on the value of the `array` parameter.
//
// Establishing the context also makes sure that the key isn't a duplicate, and
// will create implicit hashes automatically.
func (p *parser) establishContext(key Key, array bool) {
var ok bool
// Always start at the top level and drill down for our context.
hashContext := p.mapping
keyContext := make(Key, 0)
// We only need implicit hashes for key[0:-1]
for _, k := range key[0 : len(key)-1] {
_, ok = hashContext[k]
keyContext = append(keyContext, k)
// No key? Make an implicit hash and move on.
if !ok {
p.addImplicit(keyContext)
hashContext[k] = make(map[string]interface{})
}
// If the hash context is actually an array of tables, then set
// the hash context to the last element in that array.
//
// Otherwise, it better be a table, since this MUST be a key group (by
// virtue of it not being the last element in a key).
switch t := hashContext[k].(type) {
case []map[string]interface{}:
hashContext = t[len(t)-1]
case map[string]interface{}:
hashContext = t
default:
p.panicf("Key '%s' was already created as a hash.", keyContext)
}
}
p.context = keyContext
if array {
// If this is the first element for this array, then allocate a new
// list of tables for it.
k := key[len(key)-1]
if _, ok := hashContext[k]; !ok {
hashContext[k] = make([]map[string]interface{}, 0, 5)
}
// Add a new table. But make sure the key hasn't already been used
// for something else.
if hash, ok := hashContext[k].([]map[string]interface{}); ok {
hashContext[k] = append(hash, make(map[string]interface{}))
} else {
p.panicf("Key '%s' was already created and cannot be used as "+
"an array.", keyContext)
}
} else {
p.setValue(key[len(key)-1], make(map[string]interface{}))
}
p.context = append(p.context, key[len(key)-1])
}
// setValue sets the given key to the given value in the current context.
// It will make sure that the key hasn't already been defined, account for
// implicit key groups.
func (p *parser) setValue(key string, value interface{}) {
var tmpHash interface{}
var ok bool
hash := p.mapping
keyContext := make(Key, 0)
for _, k := range p.context {
keyContext = append(keyContext, k)
if tmpHash, ok = hash[k]; !ok {
p.bug("Context for key '%s' has not been established.", keyContext)
}
switch t := tmpHash.(type) {
case []map[string]interface{}:
// The context is a table of hashes. Pick the most recent table
// defined as the current hash.
hash = t[len(t)-1]
case map[string]interface{}:
hash = t
default:
p.bug("Expected hash to have type 'map[string]interface{}', but "+
"it has '%T' instead.", tmpHash)
}
}
keyContext = append(keyContext, key)
if _, ok := hash[key]; ok {
// Typically, if the given key has already been set, then we have
// to raise an error since duplicate keys are disallowed. However,
// it's possible that a key was previously defined implicitly. In this
// case, it is allowed to be redefined concretely. (See the
// `tests/valid/implicit-and-explicit-after.toml` test in `toml-test`.)
//
// But we have to make sure to stop marking it as an implicit. (So that
// another redefinition provokes an error.)
//
// Note that since it has already been defined (as a hash), we don't
// want to overwrite it. So our business is done.
if p.isImplicit(keyContext) {
p.removeImplicit(keyContext)
return
}
// Otherwise, we have a concrete key trying to override a previous
// key, which is *always* wrong.
p.panicf("Key '%s' has already been defined.", keyContext)
}
hash[key] = value
}
// setType sets the type of a particular value at a given key.
// It should be called immediately AFTER setValue.
//
// Note that if `key` is empty, then the type given will be applied to the
// current context (which is either a table or an array of tables).
func (p *parser) setType(key string, typ tomlType) {
keyContext := make(Key, 0, len(p.context)+1)
for _, k := range p.context {
keyContext = append(keyContext, k)
}
if len(key) > 0 { // allow type setting for hashes
keyContext = append(keyContext, key)
}
p.types[keyContext.String()] = typ
}
// addImplicit sets the given Key as having been created implicitly.
func (p *parser) addImplicit(key Key) {
p.implicits[key.String()] = true
}
// removeImplicit stops tagging the given key as having been implicitly created.
func (p *parser) removeImplicit(key Key) {
p.implicits[key.String()] = false
}
// isImplicit returns true if the key group pointed to by the key was created
// implicitly.
func (p *parser) isImplicit(key Key) bool {
return p.implicits[key.String()]
}
// current returns the full key name of the current context.
func (p *parser) current() string {
if len(p.currentKey) == 0 {
return p.context.String()
}
if len(p.context) == 0 {
return p.currentKey
}
return fmt.Sprintf("%s.%s", p.context, p.currentKey)
}
func replaceEscapes(s string) string {
return strings.NewReplacer(
"\\b", "\u0008",
"\\t", "\u0009",
"\\n", "\u000A",
"\\f", "\u000C",
"\\r", "\u000D",
"\\\"", "\u0022",
"\\/", "\u002F",
"\\\\", "\u005C",
).Replace(s)
}
func stripFirstNewline(s string) string {
if len(s) == 0 || s[0] != '\n' {
return s
}
return s[1:len(s)]
}
func stripEscapedWhitespace(s string) string {
esc := strings.Split(s, "\\\n")
if len(esc) > 1 {
for i := 1; i < len(esc); i++ {
esc[i] = strings.TrimLeftFunc(esc[i], unicode.IsSpace)
}
}
return strings.Join(esc, "")
}
func (p *parser) replaceUnicode(s string) string {
indexEsc := func() int {
return strings.Index(s, "\\u")
}
for i := indexEsc(); i != -1; i = indexEsc() {
asciiBytes := s[i+2 : i+6]
s = strings.Replace(s, s[i:i+6], p.asciiEscapeToUnicode(asciiBytes), -1)
}
return s
}
func (p *parser) asciiEscapeToUnicode(s string) string {
hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32)
if err != nil {
p.bug("Could not parse '%s' as a hexadecimal number, but the "+
"lexer claims it's OK: %s", s, err)
}
// BUG(burntsushi)
// I honestly don't understand how this works. I can't seem
// to find a way to make this fail. I figured this would fail on invalid
// UTF-8 characters like U+DCFF, but it doesn't.
r := string(rune(hex))
if !utf8.ValidString(r) {
p.panicf("Escaped character '\\u%s' is not valid UTF-8.", s)
}
return string(r)
}
-1
View File
@@ -1 +0,0 @@
au BufWritePost *.go silent!make tags > /dev/null 2>&1
-91
View File
@@ -1,91 +0,0 @@
package toml
// tomlType represents any Go type that corresponds to a TOML type.
// While the first draft of the TOML spec has a simplistic type system that
// probably doesn't need this level of sophistication, we seem to be militating
// toward adding real composite types.
type tomlType interface {
typeString() string
}
// typeEqual accepts any two types and returns true if they are equal.
func typeEqual(t1, t2 tomlType) bool {
if t1 == nil || t2 == nil {
return false
}
return t1.typeString() == t2.typeString()
}
func typeIsHash(t tomlType) bool {
return typeEqual(t, tomlHash) || typeEqual(t, tomlArrayHash)
}
type tomlBaseType string
func (btype tomlBaseType) typeString() string {
return string(btype)
}
func (btype tomlBaseType) String() string {
return btype.typeString()
}
var (
tomlInteger tomlBaseType = "Integer"
tomlFloat tomlBaseType = "Float"
tomlDatetime tomlBaseType = "Datetime"
tomlString tomlBaseType = "String"
tomlBool tomlBaseType = "Bool"
tomlArray tomlBaseType = "Array"
tomlHash tomlBaseType = "Hash"
tomlArrayHash tomlBaseType = "ArrayHash"
)
// typeOfPrimitive returns a tomlType of any primitive value in TOML.
// Primitive values are: Integer, Float, Datetime, String and Bool.
//
// Passing a lexer item other than the following will cause a BUG message
// to occur: itemString, itemBool, itemInteger, itemFloat, itemDatetime.
func (p *parser) typeOfPrimitive(lexItem item) tomlType {
switch lexItem.typ {
case itemInteger:
return tomlInteger
case itemFloat:
return tomlFloat
case itemDatetime:
return tomlDatetime
case itemString:
return tomlString
case itemMultilineString:
return tomlString
case itemRawString:
return tomlString
case itemRawMultilineString:
return tomlString
case itemBool:
return tomlBool
}
p.bug("Cannot infer primitive type of lex item '%s'.", lexItem)
panic("unreachable")
}
// typeOfArray returns a tomlType for an array given a list of types of its
// values.
//
// In the current spec, if an array is homogeneous, then its type is always
// "Array". If the array is not homogeneous, an error is generated.
func (p *parser) typeOfArray(types []tomlType) tomlType {
// Empty arrays are cool.
if len(types) == 0 {
return tomlArray
}
theType := types[0]
for _, t := range types[1:] {
if !typeEqual(theType, t) {
p.panicf("Array contains values of type '%s' and '%s', but arrays "+
"must be homogeneous.", theType, t)
}
}
return tomlArray
}
-241
View File
@@ -1,241 +0,0 @@
package toml
// Struct field handling is adapted from code in encoding/json:
//
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the Go distribution.
import (
"reflect"
"sort"
"sync"
)
// A field represents a single field found in a struct.
type field struct {
name string // the name of the field (`toml` tag included)
tag bool // whether field has a `toml` tag
index []int // represents the depth of an anonymous field
typ reflect.Type // the type of the field
}
// byName sorts field by name, breaking ties with depth,
// then breaking ties with "name came from toml tag", then
// breaking ties with index sequence.
type byName []field
func (x byName) Len() int { return len(x) }
func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byName) Less(i, j int) bool {
if x[i].name != x[j].name {
return x[i].name < x[j].name
}
if len(x[i].index) != len(x[j].index) {
return len(x[i].index) < len(x[j].index)
}
if x[i].tag != x[j].tag {
return x[i].tag
}
return byIndex(x).Less(i, j)
}
// byIndex sorts field by index sequence.
type byIndex []field
func (x byIndex) Len() int { return len(x) }
func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byIndex) Less(i, j int) bool {
for k, xik := range x[i].index {
if k >= len(x[j].index) {
return false
}
if xik != x[j].index[k] {
return xik < x[j].index[k]
}
}
return len(x[i].index) < len(x[j].index)
}
// typeFields returns a list of fields that TOML should recognize for the given
// type. The algorithm is breadth-first search over the set of structs to
// include - the top struct and then any reachable anonymous structs.
func typeFields(t reflect.Type) []field {
// Anonymous fields to explore at the current level and the next.
current := []field{}
next := []field{{typ: t}}
// Count of queued names for current level and the next.
count := map[reflect.Type]int{}
nextCount := map[reflect.Type]int{}
// Types already visited at an earlier level.
visited := map[reflect.Type]bool{}
// Fields found.
var fields []field
for len(next) > 0 {
current, next = next, current[:0]
count, nextCount = nextCount, map[reflect.Type]int{}
for _, f := range current {
if visited[f.typ] {
continue
}
visited[f.typ] = true
// Scan f.typ for fields to include.
for i := 0; i < f.typ.NumField(); i++ {
sf := f.typ.Field(i)
if sf.PkgPath != "" { // unexported
continue
}
name := sf.Tag.Get("toml")
if name == "-" {
continue
}
index := make([]int, len(f.index)+1)
copy(index, f.index)
index[len(f.index)] = i
ft := sf.Type
if ft.Name() == "" && ft.Kind() == reflect.Ptr {
// Follow pointer.
ft = ft.Elem()
}
// Record found field and index sequence.
if name != "" || !sf.Anonymous || ft.Kind() != reflect.Struct {
tagged := name != ""
if name == "" {
name = sf.Name
}
fields = append(fields, field{name, tagged, index, ft})
if count[f.typ] > 1 {
// If there were multiple instances, add a second,
// so that the annihilation code will see a duplicate.
// It only cares about the distinction between 1 or 2,
// so don't bother generating any more copies.
fields = append(fields, fields[len(fields)-1])
}
continue
}
// Record new anonymous struct to explore in next round.
nextCount[ft]++
if nextCount[ft] == 1 {
f := field{name: ft.Name(), index: index, typ: ft}
next = append(next, f)
}
}
}
}
sort.Sort(byName(fields))
// Delete all fields that are hidden by the Go rules for embedded fields,
// except that fields with TOML tags are promoted.
// The fields are sorted in primary order of name, secondary order
// of field index length. Loop over names; for each name, delete
// hidden fields by choosing the one dominant field that survives.
out := fields[:0]
for advance, i := 0, 0; i < len(fields); i += advance {
// One iteration per name.
// Find the sequence of fields with the name of this first field.
fi := fields[i]
name := fi.name
for advance = 1; i+advance < len(fields); advance++ {
fj := fields[i+advance]
if fj.name != name {
break
}
}
if advance == 1 { // Only one field with this name
out = append(out, fi)
continue
}
dominant, ok := dominantField(fields[i : i+advance])
if ok {
out = append(out, dominant)
}
}
fields = out
sort.Sort(byIndex(fields))
return fields
}
// dominantField looks through the fields, all of which are known to
// have the same name, to find the single field that dominates the
// others using Go's embedding rules, modified by the presence of
// TOML tags. If there are multiple top-level fields, the boolean
// will be false: This condition is an error in Go and we skip all
// the fields.
func dominantField(fields []field) (field, bool) {
// The fields are sorted in increasing index-length order. The winner
// must therefore be one with the shortest index length. Drop all
// longer entries, which is easy: just truncate the slice.
length := len(fields[0].index)
tagged := -1 // Index of first tagged field.
for i, f := range fields {
if len(f.index) > length {
fields = fields[:i]
break
}
if f.tag {
if tagged >= 0 {
// Multiple tagged fields at the same level: conflict.
// Return no field.
return field{}, false
}
tagged = i
}
}
if tagged >= 0 {
return fields[tagged], true
}
// All remaining fields have the same length. If there's more than one,
// we have a conflict (two fields named "X" at the same level) and we
// return no field.
if len(fields) > 1 {
return field{}, false
}
return fields[0], true
}
var fieldCache struct {
sync.RWMutex
m map[reflect.Type][]field
}
// cachedTypeFields is like typeFields but uses a cache to avoid repeated work.
func cachedTypeFields(t reflect.Type) []field {
fieldCache.RLock()
f := fieldCache.m[t]
fieldCache.RUnlock()
if f != nil {
return f
}
// Compute fields without lock.
// Might duplicate effort but won't hold other computations back.
f = typeFields(t)
if f == nil {
f = []field{}
}
fieldCache.Lock()
if fieldCache.m == nil {
fieldCache.m = map[reflect.Type][]field{}
}
fieldCache.m[t] = f
fieldCache.Unlock()
return f
}
@@ -1,2 +0,0 @@
Godeps/*
!Godeps/Godeps.json
-6
View File
@@ -1,6 +0,0 @@
language: go
sudo: false
go:
- 1.3
- 1.4
- tip
-174
View File
@@ -1,174 +0,0 @@
List of all the awesome people working to make Gin the best Web Framework in Go.
##gin 0.x series authors
**Original Developer:** Manu Martinez-Almeida (@manucorporat)
**Long-term Maintainer:** Javier Provecho (@javierprovecho)
People and companies, who have contributed, in alphabetical order.
**@achedeuzot (Klemen Sever)**
- Fix newline debug printing
**@adammck (Adam Mckaig)**
- Add MIT license
**@AlexanderChen1989 (Alexander)**
- Typos in README
**@alexandernyquist (Alexander Nyquist)**
- Using template.Must to fix multiple return issue
- ★ Added support for OPTIONS verb
- ★ Setting response headers before calling WriteHeader
- Improved documentation for model binding
- ★ Added Content.Redirect()
- ★ Added tons of Unit tests
**@austinheap (Austin Heap)**
- Added travis CI integration
**@andredublin (Andre Dublin)**
- Fix typo in comment
**@bredov (Ludwig Valda Vasquez)**
- Fix html templating in debug mode
**@bluele (Jun Kimura)**
- Fixes code examples in README
**@chad-russell**
- ★ Support for serializing gin.H into XML
**@dickeyxxx (Jeff Dickey)**
- Typos in README
- Add example about serving static files
**@dutchcoders (DutchCoders)**
- ★ Fix security bug that allows client to spoof ip
- Fix typo. r.HTMLTemplates -> SetHTMLTemplate
**@fmd (Fareed Dudhia)**
- Fix typo. SetHTTPTemplate -> SetHTMLTemplate
**@jammie-stackhouse (Jamie Stackhouse)**
- Add more shortcuts for router methods
**@jasonrhansen**
- Fix spelling and grammar errors in documentation
**@JasonSoft (Jason Lee)**
- Fix typo in comment
**@joiggama (Ignacio Galindo)**
- Add utf-8 charset header on renders
**@julienschmidt (Julien Schmidt)**
- gofmt the code examples
**@kelcecil (Kel Cecil)**
- Fix readme typo
**@kyledinh (Kyle Dinh)**
- Adds RunTLS()
**@LinusU (Linus Unnebäck)**
- Small fixes in README
**@loongmxbt (Saint Asky)**
- Fix typo in example
**@lucas-clemente (Lucas Clemente)**
- ★ work around path.Join removing trailing slashes from routes
**@mdigger (Dmitry Sedykh)**
- Fixes Form binding when content-type is x-www-form-urlencoded
- No repeat call c.Writer.Status() in gin.Logger
- Fixes Content-Type for json render
**@mirzac (Mirza Ceric)**
- Fix debug printing
**@mopemope (Yutaka Matsubara)**
- ★ Adds Godep support (Dependencies Manager)
- Fix variadic parameter in the flexible render API
- Fix Corrupted plain render
- Add Pluggable View Renderer Example
**@msemenistyi (Mykyta Semenistyi)**
- update Readme.md. Add code to String method
**@msoedov (Sasha Myasoedov)**
- ★ Adds tons of unit tests.
**@ngerakines (Nick Gerakines)**
- ★ Improves API, c.GET() doesn't panic
- Adds MustGet() method
**@r8k (Rajiv Kilaparti)**
- Fix Port usage in README.
**@RobAWilkinson (Robert Wilkinson)**
- Add example of forms and params
**@se77en (Damon Zhao)**
- Improve color logging
**@silasb (Silas Baronda)**
- Fixing quotes in README
**@SkuliOskarsson (Skuli Oskarsson)**
- Fixes some texts in README II
**@slimmy (Jimmy Pettersson)**
- Added messages for required bindings
**@smira (Andrey Smirnov)**
- Add support for ignored/unexported fields in binding
**@superalsrk (SRK.Lyu)**
- Update httprouter godeps
**@yosssi (Keiji Yoshida)**
- Fix link in README
**@yuyabee**
- Fixed README
-64
View File
@@ -1,64 +0,0 @@
#Changelog
###Gin 0.6 (Mar 7, 2015)
###Gin 0.5 (Feb 7, 2015)
- [NEW] Content Negotiation
- [FIX] Solved security bug that allow a client to spoof ip
- [FIX] Fix unexported/ignored fields in binding
###Gin 0.4 (Aug 21, 2014)
- [NEW] Development mode
- [NEW] Unit tests
- [NEW] Add Content.Redirect()
- [FIX] Deferring WriteHeader()
- [FIX] Improved documentation for model binding
###Gin 0.3 (Jul 18, 2014)
- [PERFORMANCE] Normal log and error log are printed in the same call.
- [PERFORMANCE] Improve performance of NoRouter()
- [PERFORMANCE] Improve context's memory locality, reduce CPU cache faults.
- [NEW] Flexible rendering API
- [NEW] Add Context.File()
- [NEW] Add shorcut RunTLS() for http.ListenAndServeTLS
- [FIX] Rename NotFound404() to NoRoute()
- [FIX] Errors in context are purged
- [FIX] Adds HEAD method in Static file serving
- [FIX] Refactors Static() file serving
- [FIX] Using keyed initialization to fix app-engine integration
- [FIX] Can't unmarshal JSON array, #63
- [FIX] Renaming Context.Req to Context.Request
- [FIX] Check application/x-www-form-urlencoded when parsing form
###Gin 0.2b (Jul 08, 2014)
- [PERFORMANCE] Using sync.Pool to allocatio/gc overhead
- [NEW] Travis CI integration
- [NEW] Completely new logger
- [NEW] New API for serving static files. gin.Static()
- [NEW] gin.H() can be serialized into XML
- [NEW] Typed errors. Errors can be typed. Internet/external/custom.
- [NEW] Support for Godeps
- [NEW] Travis/Godocs badges in README
- [NEW] New Bind() and BindWith() methods for parsing request body.
- [NEW] Add Content.Copy()
- [NEW] Add context.LastError()
- [NEW] Add shorcut for OPTIONS HTTP method
- [FIX] Tons of README fixes
- [FIX] Header is written before body
- [FIX] BasicAuth() and changes API a little bit
- [FIX] Recovery() middleware only prints panics
- [FIX] Context.Get() does not panic anymore. Use MustGet() instead.
- [FIX] Multiple http.WriteHeader() in NotFound handlers
- [FIX] Engine.Run() panics if http server can't be setted up
- [FIX] Crash when route path doesn't start with '/'
- [FIX] Do not update header when status code is negative
- [FIX] Setting response headers before calling WriteHeader in context.String()
- [FIX] Add MIT license
- [FIX] Changes behaviour of ErrorLogger() and Logger()
-10
View File
@@ -1,10 +0,0 @@
{
"ImportPath": "github.com/gin-gonic/gin",
"GoVersion": "go1.3",
"Deps": [
{
"ImportPath": "github.com/julienschmidt/httprouter",
"Rev": "00ce1c6a267162792c367acc43b1681a884e1872"
}
]
}
-21
View File
@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2014 Manuel Martínez-Almeida
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-536
View File
@@ -1,536 +0,0 @@
#Gin Web Framework [![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin)
Gin is a web framework written in Golang. It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin.
![Gin console logger](https://gin-gonic.github.io/gin/other/console.png)
```
$ cat test.go
```
```go
package main
import "github.com/gin-gonic/gin"
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.String(200, "hello world")
})
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
router.POST("/submit", func(c *gin.Context) {
c.String(401, "not authorized")
})
router.PUT("/error", func(c *gin.Context) {
c.String(500, "and error hapenned :(")
})
router.Run(":8080")
}
```
##Gin is new, will it be supported?
Yes, Gin is an internal tool of [Manu](https://github.com/manucorporat) and [Javi](https://github.com/javierprovecho) for many of our projects/start-ups. We developed it and we are going to continue using and improve it.
##Roadmap for v1.0
- [ ] Ask our designer for a cool logo
- [ ] Add tons of unit tests
- [ ] Add internal benchmarks suite
- [ ] More powerful validation API
- [ ] Improve documentation
- [ ] Add Swagger support
- [x] Stable API
- [x] Improve logging system
- [x] Improve JSON/XML validation using bindings
- [x] Improve XML support
- [x] Flexible rendering system
- [x] Add more cool middlewares, for example redis caching (this also helps developers to understand the framework).
- [x] Continuous integration
- [x] Performance improments, reduce allocation and garbage collection overhead
- [x] Fix bugs
## Start using it
Obviously, you need to have Git and Go already installed to run Gin.
Run this in your terminal
```
go get github.com/gin-gonic/gin
```
Then import it in your Go code:
```
import "github.com/gin-gonic/gin"
```
##Community
If you'd like to help out with the project, there's a mailing list and IRC channel where Gin discussions normally happen.
* IRC
* [irc.freenode.net #getgin](irc://irc.freenode.net:6667/getgin)
* [Webchat](http://webchat.freenode.net?randomnick=1&channels=%23getgin)
* Mailing List
* Subscribe: [getgin@librelist.org](mailto:getgin@librelist.org)
* [Archives](http://librelist.com/browser/getgin/)
##API Examples
#### Create most basic PING/PONG HTTP endpoint
```go
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
// Listen and server on 0.0.0.0:8080
r.Run(":8080")
}
```
#### Using GET, POST, PUT, PATCH, DELETE and OPTIONS
```go
func main() {
// Creates a gin router + logger and recovery (crash-free) middlewares
r := gin.Default()
r.GET("/someGet", getting)
r.POST("/somePost", posting)
r.PUT("/somePut", putting)
r.DELETE("/someDelete", deleting)
r.PATCH("/somePatch", patching)
r.HEAD("/someHead", head)
r.OPTIONS("/someOptions", options)
// Listen and server on 0.0.0.0:8080
r.Run(":8080")
}
```
#### Parameters in path
```go
func main() {
r := gin.Default()
// This handler will match /user/john but will not match neither /user/ or /user
r.GET("/user/:name", func(c *gin.Context) {
name := c.Params.ByName("name")
message := "Hello "+name
c.String(200, message)
})
// However, this one will match /user/john/ and also /user/john/send
// If no other routers match /user/john, it will redirect to /user/join/
r.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Params.ByName("name")
action := c.Params.ByName("action")
message := name + " is " + action
c.String(200, message)
})
// Listen and server on 0.0.0.0:8080
r.Run(":8080")
}
```
###Form parameters
```go
func main() {
r := gin.Default()
// This will respond to urls like search?firstname=Jane&lastname=Doe
r.GET("/search", func(c *gin.Context) {
// You need to call ParseForm() on the request to receive url and form params first
c.Request.ParseForm()
firstname := c.Request.Form.Get("firstname")
lastname := c.Request.Form.get("lastname")
message := "Hello "+ firstname + lastname
c.String(200, message)
})
r.Run(":8080")
}
```
#### Grouping routes
```go
func main() {
r := gin.Default()
// Simple group: v1
v1 := r.Group("/v1")
{
v1.POST("/login", loginEndpoint)
v1.POST("/submit", submitEndpoint)
v1.POST("/read", readEndpoint)
}
// Simple group: v2
v2 := r.Group("/v2")
{
v2.POST("/login", loginEndpoint)
v2.POST("/submit", submitEndpoint)
v2.POST("/read", readEndpoint)
}
// Listen and server on 0.0.0.0:8080
r.Run(":8080")
}
```
#### Blank Gin without middlewares by default
Use
```go
r := gin.New()
```
instead of
```go
r := gin.Default()
```
#### Using middlewares
```go
func main() {
// Creates a router without any middleware by default
r := gin.New()
// Global middlewares
r.Use(gin.Logger())
r.Use(gin.Recovery())
// Per route middlewares, you can add as many as you desire.
r.GET("/benchmark", MyBenchLogger(), benchEndpoint)
// Authorization group
// authorized := r.Group("/", AuthRequired())
// exactly the same than:
authorized := r.Group("/")
// per group middlewares! in this case we use the custom created
// AuthRequired() middleware just in the "authorized" group.
authorized.Use(AuthRequired())
{
authorized.POST("/login", loginEndpoint)
authorized.POST("/submit", submitEndpoint)
authorized.POST("/read", readEndpoint)
// nested group
testing := authorized.Group("testing")
testing.GET("/analytics", analyticsEndpoint)
}
// Listen and server on 0.0.0.0:8080
r.Run(":8080")
}
```
#### Model binding and validation
To bind a request body into a type, use model binding. We currently support binding of JSON, XML and standard form values (foo=bar&boo=baz).
Note that you need to set the corresponding binding tag on all fields you want to bind. For example, when binding from JSON, set `json:"fieldname"`.
When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use BindWith.
You can also specify that specific fields are required. If a field is decorated with `binding:"required"` and has a empty value when binding, the current request will fail with an error.
```go
// Binding from JSON
type LoginJSON struct {
User string `json:"user" binding:"required"`
Password string `json:"password" binding:"required"`
}
// Binding from form values
type LoginForm struct {
User string `form:"user" binding:"required"`
Password string `form:"password" binding:"required"`
}
func main() {
r := gin.Default()
// Example for binding JSON ({"user": "manu", "password": "123"})
r.POST("/loginJSON", func(c *gin.Context) {
var json LoginJSON
c.Bind(&json) // This will infer what binder to use depending on the content-type header.
if json.User == "manu" && json.Password == "123" {
c.JSON(200, gin.H{"status": "you are logged in"})
} else {
c.JSON(401, gin.H{"status": "unauthorized"})
}
})
// Example for binding a HTML form (user=manu&password=123)
r.POST("/loginHTML", func(c *gin.Context) {
var form LoginForm
c.BindWith(&form, binding.Form) // You can also specify which binder to use. We support binding.Form, binding.JSON and binding.XML.
if form.User == "manu" && form.Password == "123" {
c.JSON(200, gin.H{"status": "you are logged in"})
} else {
c.JSON(401, gin.H{"status": "unauthorized"})
}
})
// Listen and server on 0.0.0.0:8080
r.Run(":8080")
}
```
#### XML and JSON rendering
```go
func main() {
r := gin.Default()
// gin.H is a shortcut for map[string]interface{}
r.GET("/someJSON", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "hey", "status": 200})
})
r.GET("/moreJSON", func(c *gin.Context) {
// You also can use a struct
var msg struct {
Name string `json:"user"`
Message string
Number int
}
msg.Name = "Lena"
msg.Message = "hey"
msg.Number = 123
// Note that msg.Name becomes "user" in the JSON
// Will output : {"user": "Lena", "Message": "hey", "Number": 123}
c.JSON(200, msg)
})
r.GET("/someXML", func(c *gin.Context) {
c.XML(200, gin.H{"message": "hey", "status": 200})
})
// Listen and server on 0.0.0.0:8080
r.Run(":8080")
}
```
####Serving static files
Use Engine.ServeFiles(path string, root http.FileSystem):
```go
func main() {
r := gin.Default()
r.Static("/assets", "./assets")
// Listen and server on 0.0.0.0:8080
r.Run(":8080")
}
```
Note: this will use `httpNotFound` instead of the Router's `NotFound` handler.
####HTML rendering
Using LoadHTMLTemplates()
```go
func main() {
r := gin.Default()
r.LoadHTMLGlob("templates/*")
r.GET("/index", func(c *gin.Context) {
obj := gin.H{"title": "Main website"}
c.HTML(200, "index.tmpl", obj)
})
// Listen and server on 0.0.0.0:8080
r.Run(":8080")
}
```
```html
<h1>
{{ .title }}
</h1>
```
You can also use your own html template render
```go
import "html/template"
func main() {
r := gin.Default()
html := template.Must(template.ParseFiles("file1", "file2"))
r.SetHTMLTemplate(html)
// Listen and server on 0.0.0.0:8080
r.Run(":8080")
}
```
#### Redirects
Issuing a HTTP redirect is easy:
```go
r.GET("/test", func(c *gin.Context) {
c.Redirect(301, "http://www.google.com/")
})
```
Both internal and external locations are supported.
#### Custom Middlewares
```go
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
// Set example variable
c.Set("example", "12345")
// before request
c.Next()
// after request
latency := time.Since(t)
log.Print(latency)
// access the status we are sending
status := c.Writer.Status()
log.Println(status)
}
}
func main() {
r := gin.New()
r.Use(Logger())
r.GET("/test", func(c *gin.Context) {
example := c.MustGet("example").(string)
// it would print: "12345"
log.Println(example)
})
// Listen and server on 0.0.0.0:8080
r.Run(":8080")
}
```
#### Using BasicAuth() middleware
```go
// similate some private data
var secrets = gin.H{
"foo": gin.H{"email": "foo@bar.com", "phone": "123433"},
"austin": gin.H{"email": "austin@example.com", "phone": "666"},
"lena": gin.H{"email": "lena@guapa.com", "phone": "523443"},
}
func main() {
r := gin.Default()
// Group using gin.BasicAuth() middleware
// gin.Accounts is a shortcut for map[string]string
authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{
"foo": "bar",
"austin": "1234",
"lena": "hello2",
"manu": "4321",
}))
// /admin/secrets endpoint
// hit "localhost:8080/admin/secrets
authorized.GET("/secrets", func(c *gin.Context) {
// get user, it was setted by the BasicAuth middleware
user := c.MustGet(gin.AuthUserKey).(string)
if secret, ok := secrets[user]; ok {
c.JSON(200, gin.H{"user": user, "secret": secret})
} else {
c.JSON(200, gin.H{"user": user, "secret": "NO SECRET :("})
}
})
// Listen and server on 0.0.0.0:8080
r.Run(":8080")
}
```
#### Goroutines inside a middleware
When starting inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy.
```go
func main() {
r := gin.Default()
r.GET("/long_async", func(c *gin.Context) {
// create copy to be used inside the goroutine
c_cp := c.Copy()
go func() {
// simulate a long task with time.Sleep(). 5 seconds
time.Sleep(5 * time.Second)
// note than you are using the copied context "c_cp", IMPORTANT
log.Println("Done! in path " + c_cp.Request.URL.Path)
}()
})
r.GET("/long_sync", func(c *gin.Context) {
// simulate a long task with time.Sleep(). 5 seconds
time.Sleep(5 * time.Second)
// since we are NOT using a goroutine, we do not have to copy the context
log.Println("Done! in path " + c.Request.URL.Path)
})
// Listen and server on 0.0.0.0:8080
r.Run(":8080")
}
```
#### Custom HTTP configuration
Use `http.ListenAndServe()` directly, like this:
```go
func main() {
router := gin.Default()
http.ListenAndServe(":8080", router)
}
```
or
```go
func main() {
router := gin.Default()
s := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}
```
-94
View File
@@ -1,94 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"crypto/subtle"
"encoding/base64"
"errors"
"sort"
)
const (
AuthUserKey = "user"
)
type (
Accounts map[string]string
authPair struct {
Value string
User string
}
authPairs []authPair
)
func (a authPairs) Len() int { return len(a) }
func (a authPairs) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a authPairs) Less(i, j int) bool { return a[i].Value < a[j].Value }
// Implements a basic Basic HTTP Authorization. It takes as argument a map[string]string where
// the key is the user name and the value is the password.
func BasicAuth(accounts Accounts) HandlerFunc {
pairs, err := processAccounts(accounts)
if err != nil {
panic(err)
}
return func(c *Context) {
// Search user in the slice of allowed credentials
user, ok := searchCredential(pairs, c.Request.Header.Get("Authorization"))
if !ok {
// Credentials doesn't match, we return 401 Unauthorized and abort request.
c.Writer.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
c.Fail(401, errors.New("Unauthorized"))
} else {
// user is allowed, set UserId to key "user" in this context, the userId can be read later using
// c.Get(gin.AuthUserKey)
c.Set(AuthUserKey, user)
}
}
}
func processAccounts(accounts Accounts) (authPairs, error) {
if len(accounts) == 0 {
return nil, errors.New("Empty list of authorized credentials")
}
pairs := make(authPairs, 0, len(accounts))
for user, password := range accounts {
if len(user) == 0 {
return nil, errors.New("User can not be empty")
}
base := user + ":" + password
value := "Basic " + base64.StdEncoding.EncodeToString([]byte(base))
pairs = append(pairs, authPair{
Value: value,
User: user,
})
}
// We have to sort the credentials in order to use bsearch later.
sort.Sort(pairs)
return pairs, nil
}
func searchCredential(pairs authPairs, auth string) (string, bool) {
if len(auth) == 0 {
return "", false
}
// Search user in the slice of allowed credentials
r := sort.Search(len(pairs), func(i int) bool { return pairs[i].Value >= auth })
if r < len(pairs) && secureCompare(pairs[r].Value, auth) {
return pairs[r].User, true
} else {
return "", false
}
}
func secureCompare(given, actual string) bool {
if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 {
return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1
} else {
/* Securely compare actual to itself to keep constant time, but always return false */
return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false
}
}
-61
View File
@@ -1,61 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"encoding/base64"
"net/http"
"net/http/httptest"
"testing"
)
func TestBasicAuthSucceed(t *testing.T) {
req, _ := http.NewRequest("GET", "/login", nil)
w := httptest.NewRecorder()
r := New()
accounts := Accounts{"admin": "password"}
r.Use(BasicAuth(accounts))
r.GET("/login", func(c *Context) {
c.String(200, "autorized")
})
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Errorf("Response code should be Ok, was: %s", w.Code)
}
bodyAsString := w.Body.String()
if bodyAsString != "autorized" {
t.Errorf("Response body should be `autorized`, was %s", bodyAsString)
}
}
func TestBasicAuth401(t *testing.T) {
req, _ := http.NewRequest("GET", "/login", nil)
w := httptest.NewRecorder()
r := New()
accounts := Accounts{"foo": "bar"}
r.Use(BasicAuth(accounts))
r.GET("/login", func(c *Context) {
c.String(200, "autorized")
})
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
r.ServeHTTP(w, req)
if w.Code != 401 {
t.Errorf("Response code should be Not autorized, was: %s", w.Code)
}
if w.HeaderMap.Get("WWW-Authenticate") != "Basic realm=\"Authorization Required\"" {
t.Errorf("WWW-Authenticate header is incorrect: %s", w.HeaderMap.Get("Content-Type"))
}
}
-216
View File
@@ -1,216 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"encoding/json"
"encoding/xml"
"errors"
"net/http"
"reflect"
"strconv"
"strings"
)
type (
Binding interface {
Bind(*http.Request, interface{}) error
}
// JSON binding
jsonBinding struct{}
// XML binding
xmlBinding struct{}
// // form binding
formBinding struct{}
)
var (
JSON = jsonBinding{}
XML = xmlBinding{}
Form = formBinding{} // todo
)
func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error {
decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(obj); err == nil {
return Validate(obj)
} else {
return err
}
}
func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error {
decoder := xml.NewDecoder(req.Body)
if err := decoder.Decode(obj); err == nil {
return Validate(obj)
} else {
return err
}
}
func (_ formBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseForm(); err != nil {
return err
}
if err := mapForm(obj, req.Form); err != nil {
return err
}
return Validate(obj)
}
func mapForm(ptr interface{}, form map[string][]string) error {
typ := reflect.TypeOf(ptr).Elem()
formStruct := reflect.ValueOf(ptr).Elem()
for i := 0; i < typ.NumField(); i++ {
typeField := typ.Field(i)
if inputFieldName := typeField.Tag.Get("form"); inputFieldName != "" {
structField := formStruct.Field(i)
if !structField.CanSet() {
continue
}
inputValue, exists := form[inputFieldName]
if !exists {
continue
}
numElems := len(inputValue)
if structField.Kind() == reflect.Slice && numElems > 0 {
sliceOf := structField.Type().Elem().Kind()
slice := reflect.MakeSlice(structField.Type(), numElems, numElems)
for i := 0; i < numElems; i++ {
if err := setWithProperType(sliceOf, inputValue[i], slice.Index(i)); err != nil {
return err
}
}
formStruct.Field(i).Set(slice)
} else {
if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
return err
}
}
}
}
return nil
}
func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error {
switch valueKind {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if val == "" {
val = "0"
}
intVal, err := strconv.Atoi(val)
if err != nil {
return err
} else {
structField.SetInt(int64(intVal))
}
case reflect.Bool:
if val == "" {
val = "false"
}
boolVal, err := strconv.ParseBool(val)
if err != nil {
return err
} else {
structField.SetBool(boolVal)
}
case reflect.Float32:
if val == "" {
val = "0.0"
}
floatVal, err := strconv.ParseFloat(val, 32)
if err != nil {
return err
} else {
structField.SetFloat(floatVal)
}
case reflect.Float64:
if val == "" {
val = "0.0"
}
floatVal, err := strconv.ParseFloat(val, 64)
if err != nil {
return err
} else {
structField.SetFloat(floatVal)
}
case reflect.String:
structField.SetString(val)
}
return nil
}
// Don't pass in pointers to bind to. Can lead to bugs. See:
// https://github.com/codegangsta/martini-contrib/issues/40
// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659
func ensureNotPointer(obj interface{}) {
if reflect.TypeOf(obj).Kind() == reflect.Ptr {
panic("Pointers are not accepted as binding models")
}
}
func Validate(obj interface{}, parents ...string) error {
typ := reflect.TypeOf(obj)
val := reflect.ValueOf(obj)
if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
val = val.Elem()
}
switch typ.Kind() {
case reflect.Struct:
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
// Allow ignored and unexported fields in the struct
if len(field.PkgPath) > 0 || field.Tag.Get("form") == "-" {
continue
}
fieldValue := val.Field(i).Interface()
zero := reflect.Zero(field.Type).Interface()
if strings.Index(field.Tag.Get("binding"), "required") > -1 {
fieldType := field.Type.Kind()
if fieldType == reflect.Struct {
if reflect.DeepEqual(zero, fieldValue) {
return errors.New("Required " + field.Name)
}
err := Validate(fieldValue, field.Name)
if err != nil {
return err
}
} else if reflect.DeepEqual(zero, fieldValue) {
if len(parents) > 0 {
return errors.New("Required " + field.Name + " on " + parents[0])
} else {
return errors.New("Required " + field.Name)
}
} else if fieldType == reflect.Slice && field.Type.Elem().Kind() == reflect.Struct {
err := Validate(fieldValue)
if err != nil {
return err
}
}
}
}
case reflect.Slice:
for i := 0; i < val.Len(); i++ {
fieldValue := val.Index(i).Interface()
err := Validate(fieldValue)
if err != nil {
return err
}
}
default:
return nil
}
return nil
}
-434
View File
@@ -1,434 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"bytes"
"errors"
"fmt"
"github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/render"
"github.com/julienschmidt/httprouter"
"log"
"net"
"net/http"
"strings"
)
const (
ErrorTypeInternal = 1 << iota
ErrorTypeExternal = 1 << iota
ErrorTypeAll = 0xffffffff
)
// Used internally to collect errors that occurred during an http request.
type errorMsg struct {
Err string `json:"error"`
Type uint32 `json:"-"`
Meta interface{} `json:"meta"`
}
type errorMsgs []errorMsg
func (a errorMsgs) ByType(typ uint32) errorMsgs {
if len(a) == 0 {
return a
}
result := make(errorMsgs, 0, len(a))
for _, msg := range a {
if msg.Type&typ > 0 {
result = append(result, msg)
}
}
return result
}
func (a errorMsgs) String() string {
if len(a) == 0 {
return ""
}
var buffer bytes.Buffer
for i, msg := range a {
text := fmt.Sprintf("Error #%02d: %s \n Meta: %v\n", (i + 1), msg.Err, msg.Meta)
buffer.WriteString(text)
}
return buffer.String()
}
// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {
writermem responseWriter
Request *http.Request
Writer ResponseWriter
Keys map[string]interface{}
Errors errorMsgs
Params httprouter.Params
Engine *Engine
handlers []HandlerFunc
index int8
accepted []string
}
/************************************/
/********** CONTEXT CREATION ********/
/************************************/
func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.Params = params
c.handlers = handlers
c.Keys = nil
c.index = -1
c.accepted = nil
c.Errors = c.Errors[0:0]
return c
}
func (engine *Engine) reuseContext(c *Context) {
engine.pool.Put(c)
}
func (c *Context) Copy() *Context {
var cp Context = *c
cp.index = AbortIndex
cp.handlers = nil
return &cp
}
/************************************/
/*************** FLOW ***************/
/************************************/
// Next should be used only in the middlewares.
// It executes the pending handlers in the chain inside the calling handler.
// See example in github.
func (c *Context) Next() {
c.index++
s := int8(len(c.handlers))
for ; c.index < s; c.index++ {
c.handlers[c.index](c)
}
}
// Forces the system to do not continue calling the pending handlers in the chain.
func (c *Context) Abort() {
c.index = AbortIndex
}
// Same than AbortWithStatus() but also writes the specified response status code.
// For example, the first handler checks if the request is authorized. If it's not, context.AbortWithStatus(401) should be called.
func (c *Context) AbortWithStatus(code int) {
c.Writer.WriteHeader(code)
c.Abort()
}
/************************************/
/********* ERROR MANAGEMENT *********/
/************************************/
// Fail is the same as Abort plus an error message.
// Calling `context.Fail(500, err)` is equivalent to:
// ```
// context.Error("Operation aborted", err)
// context.AbortWithStatus(500)
// ```
func (c *Context) Fail(code int, err error) {
c.Error(err, "Operation aborted")
c.AbortWithStatus(code)
}
func (c *Context) ErrorTyped(err error, typ uint32, meta interface{}) {
c.Errors = append(c.Errors, errorMsg{
Err: err.Error(),
Type: typ,
Meta: meta,
})
}
// Attaches an error to the current context. The error is pushed to a list of errors.
// It's a good idea to call Error for each error that occurred during the resolution of a request.
// A middleware can be used to collect all the errors and push them to a database together, print a log, or append it in the HTTP response.
func (c *Context) Error(err error, meta interface{}) {
c.ErrorTyped(err, ErrorTypeExternal, meta)
}
func (c *Context) LastError() error {
nuErrors := len(c.Errors)
if nuErrors > 0 {
return errors.New(c.Errors[nuErrors-1].Err)
} else {
return nil
}
}
/************************************/
/******** METADATA MANAGEMENT********/
/************************************/
// Sets a new pair key/value just for the specified context.
// It also lazy initializes the hashmap.
func (c *Context) Set(key string, item interface{}) {
if c.Keys == nil {
c.Keys = make(map[string]interface{})
}
c.Keys[key] = item
}
// Get returns the value for the given key or an error if the key does not exist.
func (c *Context) Get(key string) (interface{}, error) {
if c.Keys != nil {
value, ok := c.Keys[key]
if ok {
return value, nil
}
}
return nil, errors.New("Key does not exist.")
}
// MustGet returns the value for the given key or panics if the value doesn't exist.
func (c *Context) MustGet(key string) interface{} {
value, err := c.Get(key)
if err != nil || value == nil {
log.Panicf("Key %s doesn't exist", value)
}
return value
}
func ipInMasks(ip net.IP, masks []interface{}) bool {
for _, proxy := range masks {
var mask *net.IPNet
var err error
switch t := proxy.(type) {
case string:
if _, mask, err = net.ParseCIDR(t); err != nil {
panic(err)
}
case net.IP:
mask = &net.IPNet{IP: t, Mask: net.CIDRMask(len(t)*8, len(t)*8)}
case net.IPNet:
mask = &t
}
if mask.Contains(ip) {
return true
}
}
return false
}
// the ForwardedFor middleware unwraps the X-Forwarded-For headers, be careful to only use this
// middleware if you've got servers in front of this server. The list with (known) proxies and
// local ips are being filtered out of the forwarded for list, giving the last not local ip being
// the real client ip.
func ForwardedFor(proxies ...interface{}) HandlerFunc {
if len(proxies) == 0 {
// default to local ips
var reservedLocalIps = []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}
proxies = make([]interface{}, len(reservedLocalIps))
for i, v := range reservedLocalIps {
proxies[i] = v
}
}
return func(c *Context) {
// the X-Forwarded-For header contains an array with left most the client ip, then
// comma separated, all proxies the request passed. The last proxy appears
// as the remote address of the request. Returning the client
// ip to comply with default RemoteAddr response.
// check if remoteaddr is local ip or in list of defined proxies
remoteIp := net.ParseIP(strings.Split(c.Request.RemoteAddr, ":")[0])
if !ipInMasks(remoteIp, proxies) {
return
}
if forwardedFor := c.Request.Header.Get("X-Forwarded-For"); forwardedFor != "" {
parts := strings.Split(forwardedFor, ",")
for i := len(parts) - 1; i >= 0; i-- {
part := parts[i]
ip := net.ParseIP(strings.TrimSpace(part))
if ipInMasks(ip, proxies) {
continue
}
// returning remote addr conform the original remote addr format
c.Request.RemoteAddr = ip.String() + ":0"
// remove forwarded for address
c.Request.Header.Set("X-Forwarded-For", "")
return
}
}
}
}
func (c *Context) ClientIP() string {
return c.Request.RemoteAddr
}
/************************************/
/********* PARSING REQUEST **********/
/************************************/
// This function checks the Content-Type to select a binding engine automatically,
// Depending the "Content-Type" header different bindings are used:
// "application/json" --> JSON binding
// "application/xml" --> XML binding
// else --> returns an error
// if Parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. It decodes the json payload into the struct specified as a pointer.Like ParseBody() but this method also writes a 400 error if the json is not valid.
func (c *Context) Bind(obj interface{}) bool {
var b binding.Binding
ctype := filterFlags(c.Request.Header.Get("Content-Type"))
switch {
case c.Request.Method == "GET" || ctype == MIMEPOSTForm:
b = binding.Form
case ctype == MIMEJSON:
b = binding.JSON
case ctype == MIMEXML || ctype == MIMEXML2:
b = binding.XML
default:
c.Fail(400, errors.New("unknown content-type: "+ctype))
return false
}
return c.BindWith(obj, b)
}
func (c *Context) BindWith(obj interface{}, b binding.Binding) bool {
if err := b.Bind(c.Request, obj); err != nil {
c.Fail(400, err)
return false
}
return true
}
/************************************/
/******** RESPONSE RENDERING ********/
/************************************/
func (c *Context) Render(code int, render render.Render, obj ...interface{}) {
if err := render.Render(c.Writer, code, obj...); err != nil {
c.ErrorTyped(err, ErrorTypeInternal, obj)
c.AbortWithStatus(500)
}
}
// Serializes the given struct as JSON into the response body in a fast and efficient way.
// It also sets the Content-Type as "application/json".
func (c *Context) JSON(code int, obj interface{}) {
c.Render(code, render.JSON, obj)
}
// Serializes the given struct as XML into the response body in a fast and efficient way.
// It also sets the Content-Type as "application/xml".
func (c *Context) XML(code int, obj interface{}) {
c.Render(code, render.XML, obj)
}
// Renders the HTTP template specified by its file name.
// It also updates the HTTP code and sets the Content-Type as "text/html".
// See http://golang.org/doc/articles/wiki/
func (c *Context) HTML(code int, name string, obj interface{}) {
c.Render(code, c.Engine.HTMLRender, name, obj)
}
// Writes the given string into the response body and sets the Content-Type to "text/plain".
func (c *Context) String(code int, format string, values ...interface{}) {
c.Render(code, render.Plain, format, values)
}
// Returns a HTTP redirect to the specific location.
func (c *Context) Redirect(code int, location string) {
if code >= 300 && code <= 308 {
c.Render(code, render.Redirect, location)
} else {
panic(fmt.Sprintf("Cannot send a redirect with status code %d", code))
}
}
// Writes some data into the body stream and updates the HTTP code.
func (c *Context) Data(code int, contentType string, data []byte) {
if len(contentType) > 0 {
c.Writer.Header().Set("Content-Type", contentType)
}
c.Writer.WriteHeader(code)
c.Writer.Write(data)
}
// Writes the specified file into the body stream
func (c *Context) File(filepath string) {
http.ServeFile(c.Writer, c.Request, filepath)
}
/************************************/
/******** CONTENT NEGOTIATION *******/
/************************************/
type Negotiate struct {
Offered []string
HTMLPath string
HTMLData interface{}
JSONData interface{}
XMLData interface{}
Data interface{}
}
func (c *Context) Negotiate(code int, config Negotiate) {
switch c.NegotiateFormat(config.Offered...) {
case MIMEJSON:
data := chooseData(config.JSONData, config.Data)
c.JSON(code, data)
case MIMEHTML:
data := chooseData(config.HTMLData, config.Data)
if len(config.HTMLPath) == 0 {
panic("negotiate config is wrong. html path is needed")
}
c.HTML(code, config.HTMLPath, data)
case MIMEXML:
data := chooseData(config.XMLData, config.Data)
c.XML(code, data)
default:
c.Fail(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server"))
}
}
func (c *Context) NegotiateFormat(offered ...string) string {
if len(offered) == 0 {
panic("you must provide at least one offer")
}
if c.accepted == nil {
c.accepted = parseAccept(c.Request.Header.Get("Accept"))
}
if len(c.accepted) == 0 {
return offered[0]
} else {
for _, accepted := range c.accepted {
for _, offert := range offered {
if accepted == offert {
return offert
}
}
}
return ""
}
}
func (c *Context) SetAccepted(formats ...string) {
c.accepted = formats
}
-483
View File
@@ -1,483 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"bytes"
"errors"
"html/template"
"net/http"
"net/http/httptest"
"testing"
)
// TestContextParamsGet tests that a parameter can be parsed from the URL.
func TestContextParamsByName(t *testing.T) {
req, _ := http.NewRequest("GET", "/test/alexandernyquist", nil)
w := httptest.NewRecorder()
name := ""
r := New()
r.GET("/test/:name", func(c *Context) {
name = c.Params.ByName("name")
})
r.ServeHTTP(w, req)
if name != "alexandernyquist" {
t.Errorf("Url parameter was not correctly parsed. Should be alexandernyquist, was %s.", name)
}
}
// TestContextSetGet tests that a parameter is set correctly on the
// current context and can be retrieved using Get.
func TestContextSetGet(t *testing.T) {
req, _ := http.NewRequest("GET", "/test", nil)
w := httptest.NewRecorder()
r := New()
r.GET("/test", func(c *Context) {
// Key should be lazily created
if c.Keys != nil {
t.Error("Keys should be nil")
}
// Set
c.Set("foo", "bar")
v, err := c.Get("foo")
if err != nil {
t.Errorf("Error on exist key")
}
if v != "bar" {
t.Errorf("Value should be bar, was %s", v)
}
})
r.ServeHTTP(w, req)
}
// TestContextJSON tests that the response is serialized as JSON
// and Content-Type is set to application/json
func TestContextJSON(t *testing.T) {
req, _ := http.NewRequest("GET", "/test", nil)
w := httptest.NewRecorder()
r := New()
r.GET("/test", func(c *Context) {
c.JSON(200, H{"foo": "bar"})
})
r.ServeHTTP(w, req)
if w.Body.String() != "{\"foo\":\"bar\"}\n" {
t.Errorf("Response should be {\"foo\":\"bar\"}, was: %s", w.Body.String())
}
if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" {
t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type"))
}
}
// TestContextHTML tests that the response executes the templates
// and responds with Content-Type set to text/html
func TestContextHTML(t *testing.T) {
req, _ := http.NewRequest("GET", "/test", nil)
w := httptest.NewRecorder()
r := New()
templ, _ := template.New("t").Parse(`Hello {{.Name}}`)
r.SetHTMLTemplate(templ)
type TestData struct{ Name string }
r.GET("/test", func(c *Context) {
c.HTML(200, "t", TestData{"alexandernyquist"})
})
r.ServeHTTP(w, req)
if w.Body.String() != "Hello alexandernyquist" {
t.Errorf("Response should be Hello alexandernyquist, was: %s", w.Body.String())
}
if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" {
t.Errorf("Content-Type should be text/html, was %s", w.HeaderMap.Get("Content-Type"))
}
}
// TestContextString tests that the response is returned
// with Content-Type set to text/plain
func TestContextString(t *testing.T) {
req, _ := http.NewRequest("GET", "/test", nil)
w := httptest.NewRecorder()
r := New()
r.GET("/test", func(c *Context) {
c.String(200, "test")
})
r.ServeHTTP(w, req)
if w.Body.String() != "test" {
t.Errorf("Response should be test, was: %s", w.Body.String())
}
if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" {
t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type"))
}
}
// TestContextXML tests that the response is serialized as XML
// and Content-Type is set to application/xml
func TestContextXML(t *testing.T) {
req, _ := http.NewRequest("GET", "/test", nil)
w := httptest.NewRecorder()
r := New()
r.GET("/test", func(c *Context) {
c.XML(200, H{"foo": "bar"})
})
r.ServeHTTP(w, req)
if w.Body.String() != "<map><foo>bar</foo></map>" {
t.Errorf("Response should be <map><foo>bar</foo></map>, was: %s", w.Body.String())
}
if w.HeaderMap.Get("Content-Type") != "application/xml; charset=utf-8" {
t.Errorf("Content-Type should be application/xml, was %s", w.HeaderMap.Get("Content-Type"))
}
}
// TestContextData tests that the response can be written from `bytesting`
// with specified MIME type
func TestContextData(t *testing.T) {
req, _ := http.NewRequest("GET", "/test/csv", nil)
w := httptest.NewRecorder()
r := New()
r.GET("/test/csv", func(c *Context) {
c.Data(200, "text/csv", []byte(`foo,bar`))
})
r.ServeHTTP(w, req)
if w.Body.String() != "foo,bar" {
t.Errorf("Response should be foo&bar, was: %s", w.Body.String())
}
if w.HeaderMap.Get("Content-Type") != "text/csv" {
t.Errorf("Content-Type should be text/csv, was %s", w.HeaderMap.Get("Content-Type"))
}
}
func TestContextFile(t *testing.T) {
req, _ := http.NewRequest("GET", "/test/file", nil)
w := httptest.NewRecorder()
r := New()
r.GET("/test/file", func(c *Context) {
c.File("./gin.go")
})
r.ServeHTTP(w, req)
bodyAsString := w.Body.String()
if len(bodyAsString) == 0 {
t.Errorf("Got empty body instead of file data")
}
if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" {
t.Errorf("Content-Type should be text/plain; charset=utf-8, was %s", w.HeaderMap.Get("Content-Type"))
}
}
// TestHandlerFunc - ensure that custom middleware works properly
func TestHandlerFunc(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
r := New()
var stepsPassed int = 0
r.Use(func(context *Context) {
stepsPassed += 1
context.Next()
stepsPassed += 1
})
r.ServeHTTP(w, req)
if w.Code != 404 {
t.Errorf("Response code should be Not found, was: %s", w.Code)
}
if stepsPassed != 2 {
t.Errorf("Falied to switch context in handler function: %s", stepsPassed)
}
}
// TestBadAbortHandlersChain - ensure that Abort after switch context will not interrupt pending handlers
func TestBadAbortHandlersChain(t *testing.T) {
// SETUP
var stepsPassed int = 0
r := New()
r.Use(func(c *Context) {
stepsPassed += 1
c.Next()
stepsPassed += 1
// after check and abort
c.AbortWithStatus(409)
})
r.Use(func(c *Context) {
stepsPassed += 1
c.Next()
stepsPassed += 1
c.AbortWithStatus(403)
})
// RUN
w := PerformRequest(r, "GET", "/")
// TEST
if w.Code != 409 {
t.Errorf("Response code should be Forbiden, was: %d", w.Code)
}
if stepsPassed != 4 {
t.Errorf("Falied to switch context in handler function: %d", stepsPassed)
}
}
// TestAbortHandlersChain - ensure that Abort interrupt used middlewares in fifo order
func TestAbortHandlersChain(t *testing.T) {
// SETUP
var stepsPassed int = 0
r := New()
r.Use(func(context *Context) {
stepsPassed += 1
context.AbortWithStatus(409)
})
r.Use(func(context *Context) {
stepsPassed += 1
context.Next()
stepsPassed += 1
})
// RUN
w := PerformRequest(r, "GET", "/")
// TEST
if w.Code != 409 {
t.Errorf("Response code should be Conflict, was: %d", w.Code)
}
if stepsPassed != 1 {
t.Errorf("Falied to switch context in handler function: %d", stepsPassed)
}
}
// TestFailHandlersChain - ensure that Fail interrupt used middlewares in fifo order as
// as well as Abort
func TestFailHandlersChain(t *testing.T) {
// SETUP
var stepsPassed int = 0
r := New()
r.Use(func(context *Context) {
stepsPassed += 1
context.Fail(500, errors.New("foo"))
})
r.Use(func(context *Context) {
stepsPassed += 1
context.Next()
stepsPassed += 1
})
// RUN
w := PerformRequest(r, "GET", "/")
// TEST
if w.Code != 500 {
t.Errorf("Response code should be Server error, was: %d", w.Code)
}
if stepsPassed != 1 {
t.Errorf("Falied to switch context in handler function: %d", stepsPassed)
}
}
func TestBindingJSON(t *testing.T) {
body := bytes.NewBuffer([]byte("{\"foo\":\"bar\"}"))
r := New()
r.POST("/binding/json", func(c *Context) {
var body struct {
Foo string `json:"foo"`
}
if c.Bind(&body) {
c.JSON(200, H{"parsed": body.Foo})
}
})
req, _ := http.NewRequest("POST", "/binding/json", body)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Errorf("Response code should be Ok, was: %s", w.Code)
}
if w.Body.String() != "{\"parsed\":\"bar\"}\n" {
t.Errorf("Response should be {\"parsed\":\"bar\"}, was: %s", w.Body.String())
}
if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" {
t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type"))
}
}
func TestBindingJSONEncoding(t *testing.T) {
body := bytes.NewBuffer([]byte("{\"foo\":\"嘉\"}"))
r := New()
r.POST("/binding/json", func(c *Context) {
var body struct {
Foo string `json:"foo"`
}
if c.Bind(&body) {
c.JSON(200, H{"parsed": body.Foo})
}
})
req, _ := http.NewRequest("POST", "/binding/json", body)
req.Header.Set("Content-Type", "application/json; charset=utf-8")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Errorf("Response code should be Ok, was: %s", w.Code)
}
if w.Body.String() != "{\"parsed\":\"嘉\"}\n" {
t.Errorf("Response should be {\"parsed\":\"嘉\"}, was: %s", w.Body.String())
}
if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" {
t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type"))
}
}
func TestBindingJSONNoContentType(t *testing.T) {
body := bytes.NewBuffer([]byte("{\"foo\":\"bar\"}"))
r := New()
r.POST("/binding/json", func(c *Context) {
var body struct {
Foo string `json:"foo"`
}
if c.Bind(&body) {
c.JSON(200, H{"parsed": body.Foo})
}
})
req, _ := http.NewRequest("POST", "/binding/json", body)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != 400 {
t.Errorf("Response code should be Bad request, was: %s", w.Code)
}
if w.Body.String() == "{\"parsed\":\"bar\"}\n" {
t.Errorf("Response should not be {\"parsed\":\"bar\"}, was: %s", w.Body.String())
}
if w.HeaderMap.Get("Content-Type") == "application/json" {
t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type"))
}
}
func TestBindingJSONMalformed(t *testing.T) {
body := bytes.NewBuffer([]byte("\"foo\":\"bar\"\n"))
r := New()
r.POST("/binding/json", func(c *Context) {
var body struct {
Foo string `json:"foo"`
}
if c.Bind(&body) {
c.JSON(200, H{"parsed": body.Foo})
}
})
req, _ := http.NewRequest("POST", "/binding/json", body)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != 400 {
t.Errorf("Response code should be Bad request, was: %s", w.Code)
}
if w.Body.String() == "{\"parsed\":\"bar\"}\n" {
t.Errorf("Response should not be {\"parsed\":\"bar\"}, was: %s", w.Body.String())
}
if w.HeaderMap.Get("Content-Type") == "application/json" {
t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type"))
}
}
func TestClientIP(t *testing.T) {
r := New()
var clientIP string = ""
r.GET("/", func(c *Context) {
clientIP = c.ClientIP()
})
body := bytes.NewBuffer([]byte(""))
req, _ := http.NewRequest("GET", "/", body)
req.RemoteAddr = "clientip:1234"
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if clientIP != "clientip:1234" {
t.Errorf("ClientIP should not be %s, but clientip:1234", clientIP)
}
}
func TestClientIPWithXForwardedForWithProxy(t *testing.T) {
r := New()
r.Use(ForwardedFor())
var clientIP string = ""
r.GET("/", func(c *Context) {
clientIP = c.ClientIP()
})
body := bytes.NewBuffer([]byte(""))
req, _ := http.NewRequest("GET", "/", body)
req.RemoteAddr = "172.16.8.3:1234"
req.Header.Set("X-Real-Ip", "realip")
req.Header.Set("X-Forwarded-For", "1.2.3.4, 10.10.0.4, 192.168.0.43, 172.16.8.4")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if clientIP != "1.2.3.4:0" {
t.Errorf("ClientIP should not be %s, but 1.2.3.4:0", clientIP)
}
}
-47
View File
@@ -1,47 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"github.com/gin-gonic/gin/binding"
"net/http"
)
// DEPRECATED, use Bind() instead.
// Like ParseBody() but this method also writes a 400 error if the json is not valid.
func (c *Context) EnsureBody(item interface{}) bool {
return c.Bind(item)
}
// DEPRECATED use bindings directly
// Parses the body content as a JSON input. It decodes the json payload into the struct specified as a pointer.
func (c *Context) ParseBody(item interface{}) error {
return binding.JSON.Bind(c.Request, item)
}
// DEPRECATED use gin.Static() instead
// ServeFiles serves files from the given file system root.
// The path must end with "/*filepath", files are then served from the local
// path /defined/root/dir/*filepath.
// For example if root is "/etc" and *filepath is "passwd", the local file
// "/etc/passwd" would be served.
// Internally a http.FileServer is used, therefore http.NotFound is used instead
// of the Router's NotFound handler.
// To use the operating system's file system implementation,
// use http.Dir:
// router.ServeFiles("/src/*filepath", http.Dir("/var/www"))
func (engine *Engine) ServeFiles(path string, root http.FileSystem) {
engine.router.ServeFiles(path, root)
}
// DEPRECATED use gin.LoadHTMLGlob() or gin.LoadHTMLFiles() instead
func (engine *Engine) LoadHTMLTemplates(pattern string) {
engine.LoadHTMLGlob(pattern)
}
// DEPRECATED. Use NoRoute() instead
func (engine *Engine) NotFound404(handlers ...HandlerFunc) {
engine.NoRoute(handlers...)
}
@@ -1,7 +0,0 @@
# Guide to run Gin under App Engine LOCAL Development Server
1. Download, install and setup Go in your computer. (That includes setting your `$GOPATH`.)
2. Download SDK for your platform from here: `https://developers.google.com/appengine/downloads?hl=es#Google_App_Engine_SDK_for_Go`
3. Download Gin source code using: `$ go get github.com/gin-gonic/gin`
4. Navigate to examples folder: `$ cd $GOPATH/src/github.com/gin-gonic/gin/examples/`
5. Run it: `$ goapp serve app-engine/`
@@ -1,8 +0,0 @@
application: hello
version: 1
runtime: go
api_version: go1
handlers:
- url: /.*
script: _go_app
@@ -1,23 +0,0 @@
package hello
import (
"net/http"
"github.com/gin-gonic/gin"
)
// This function's name is a must. App Engine uses it to drive the requests properly.
func init() {
// Starts a new Gin instance with no middle-ware
r := gin.New()
// Define your handlers
r.GET("/", func(c *gin.Context){
c.String(200, "Hello World!")
})
r.GET("/ping", func(c *gin.Context){
c.String(200, "pong")
})
// Handle all requests using net/http
http.Handle("/", r)
}
@@ -1,56 +0,0 @@
package main
import (
"github.com/gin-gonic/gin"
)
var DB = make(map[string]string)
func main() {
r := gin.Default()
// Ping test
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
// Get user value
r.GET("/user/:name", func(c *gin.Context) {
user := c.Params.ByName("name")
value, ok := DB[user]
if ok {
c.JSON(200, gin.H{"user": user, "value": value})
} else {
c.JSON(200, gin.H{"user": user, "status": "no value"})
}
})
// Authorized group (uses gin.BasicAuth() middleware)
// Same than:
// authorized := r.Group("/")
// authorized.Use(gin.BasicAuth(gin.Credentials{
// "foo": "bar",
// "manu": "123",
//}))
authorized := r.Group("/", gin.BasicAuth(gin.Accounts{
"foo": "bar", // user:foo password:bar
"manu": "123", // user:manu password:123
}))
authorized.POST("admin", func(c *gin.Context) {
user := c.MustGet(gin.AuthUserKey).(string)
// Parse JSON
var json struct {
Value string `json:"value" binding:"required"`
}
if c.Bind(&json) {
DB[user] = json.Value
c.JSON(200, gin.H{"status": "ok"})
}
})
// Listen and Server in 0.0.0.0:8080
r.Run(":8080")
}
@@ -1,58 +0,0 @@
package main
import (
"github.com/flosch/pongo2"
"github.com/gin-gonic/gin"
"net/http"
)
type pongoRender struct {
cache map[string]*pongo2.Template
}
func newPongoRender() *pongoRender {
return &pongoRender{map[string]*pongo2.Template{}}
}
func writeHeader(w http.ResponseWriter, code int, contentType string) {
if code >= 0 {
w.Header().Set("Content-Type", contentType)
w.WriteHeader(code)
}
}
func (p *pongoRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
file := data[0].(string)
ctx := data[1].(pongo2.Context)
var t *pongo2.Template
if tmpl, ok := p.cache[file]; ok {
t = tmpl
} else {
tmpl, err := pongo2.FromFile(file)
if err != nil {
return err
}
p.cache[file] = tmpl
t = tmpl
}
writeHeader(w, code, "text/html")
return t.ExecuteWriter(ctx, w)
}
func main() {
r := gin.Default()
r.HTMLRender = newPongoRender()
r.GET("/index", func(c *gin.Context) {
name := c.Request.FormValue("name")
ctx := pongo2.Context{
"title": "Gin meets pongo2 !",
"name": name,
}
c.HTML(200, "index.html", ctx)
})
// Listen and server on 0.0.0.0:8080
r.Run(":8080")
}
@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>{{ title }}</title>
<meta name="keywords" content="">
<meta name="description" content="">
</head>
<body>
Hello {{ name }} !
</body>
</html>
-143
View File
@@ -1,143 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"github.com/gin-gonic/gin/render"
"github.com/julienschmidt/httprouter"
"html/template"
"math"
"net/http"
"sync"
)
const (
AbortIndex = math.MaxInt8 / 2
MIMEJSON = "application/json"
MIMEHTML = "text/html"
MIMEXML = "application/xml"
MIMEXML2 = "text/xml"
MIMEPlain = "text/plain"
MIMEPOSTForm = "application/x-www-form-urlencoded"
)
type (
HandlerFunc func(*Context)
// Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares.
Engine struct {
*RouterGroup
HTMLRender render.Render
Default404Body []byte
pool sync.Pool
allNoRoute []HandlerFunc
noRoute []HandlerFunc
router *httprouter.Router
}
)
// Returns a new blank Engine instance without any middleware attached.
// The most basic configuration
func New() *Engine {
engine := &Engine{}
engine.RouterGroup = &RouterGroup{
Handlers: nil,
absolutePath: "/",
engine: engine,
}
engine.router = httprouter.New()
engine.Default404Body = []byte("404 page not found")
engine.router.NotFound = engine.handle404
engine.pool.New = func() interface{} {
c := &Context{Engine: engine}
c.Writer = &c.writermem
return c
}
return engine
}
// Returns a Engine instance with the Logger and Recovery already attached.
func Default() *Engine {
engine := New()
engine.Use(Recovery(), Logger())
return engine
}
func (engine *Engine) LoadHTMLGlob(pattern string) {
if IsDebugging() {
render.HTMLDebug.AddGlob(pattern)
engine.HTMLRender = render.HTMLDebug
} else {
templ := template.Must(template.ParseGlob(pattern))
engine.SetHTMLTemplate(templ)
}
}
func (engine *Engine) LoadHTMLFiles(files ...string) {
if IsDebugging() {
render.HTMLDebug.AddFiles(files...)
engine.HTMLRender = render.HTMLDebug
} else {
templ := template.Must(template.ParseFiles(files...))
engine.SetHTMLTemplate(templ)
}
}
func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
engine.HTMLRender = render.HTMLRender{
Template: templ,
}
}
// Adds handlers for NoRoute. It return a 404 code by default.
func (engine *Engine) NoRoute(handlers ...HandlerFunc) {
engine.noRoute = handlers
engine.rebuild404Handlers()
}
func (engine *Engine) Use(middlewares ...HandlerFunc) {
engine.RouterGroup.Use(middlewares...)
engine.rebuild404Handlers()
}
func (engine *Engine) rebuild404Handlers() {
engine.allNoRoute = engine.combineHandlers(engine.noRoute)
}
func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) {
c := engine.createContext(w, req, nil, engine.allNoRoute)
// set 404 by default, useful for logging
c.Writer.WriteHeader(404)
c.Next()
if !c.Writer.Written() {
if c.Writer.Status() == 404 {
c.Data(-1, MIMEPlain, engine.Default404Body)
} else {
c.Writer.WriteHeaderNow()
}
}
engine.reuseContext(c)
}
// ServeHTTP makes the router implement the http.Handler interface.
func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
engine.router.ServeHTTP(writer, request)
}
func (engine *Engine) Run(addr string) error {
debugPrint("Listening and serving HTTP on %s\n", addr)
if err := http.ListenAndServe(addr, engine); err != nil {
return err
}
return nil
}
func (engine *Engine) RunTLS(addr string, cert string, key string) error {
debugPrint("Listening and serving HTTPS on %s\n", addr)
if err := http.ListenAndServeTLS(addr, cert, key, engine); err != nil {
return err
}
return nil
}
-206
View File
@@ -1,206 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path"
"strings"
"testing"
)
func init() {
SetMode(TestMode)
}
func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
req, _ := http.NewRequest(method, path, nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
return w
}
// TestSingleRouteOK tests that POST route is correctly invoked.
func testRouteOK(method string, t *testing.T) {
// SETUP
passed := false
r := New()
r.Handle(method, "/test", []HandlerFunc{func(c *Context) {
passed = true
}})
// RUN
w := PerformRequest(r, method, "/test")
// TEST
if passed == false {
t.Errorf(method + " route handler was not invoked.")
}
if w.Code != http.StatusOK {
t.Errorf("Status code should be %v, was %d", http.StatusOK, w.Code)
}
}
func TestRouterGroupRouteOK(t *testing.T) {
testRouteOK("POST", t)
testRouteOK("DELETE", t)
testRouteOK("PATCH", t)
testRouteOK("PUT", t)
testRouteOK("OPTIONS", t)
testRouteOK("HEAD", t)
}
// TestSingleRouteOK tests that POST route is correctly invoked.
func testRouteNotOK(method string, t *testing.T) {
// SETUP
passed := false
r := New()
r.Handle(method, "/test_2", []HandlerFunc{func(c *Context) {
passed = true
}})
// RUN
w := PerformRequest(r, method, "/test")
// TEST
if passed == true {
t.Errorf(method + " route handler was invoked, when it should not")
}
if w.Code != http.StatusNotFound {
// If this fails, it's because httprouter needs to be updated to at least f78f58a0db
t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusNotFound, w.Code, w.HeaderMap.Get("Location"))
}
}
// TestSingleRouteOK tests that POST route is correctly invoked.
func TestRouteNotOK(t *testing.T) {
testRouteNotOK("POST", t)
testRouteNotOK("DELETE", t)
testRouteNotOK("PATCH", t)
testRouteNotOK("PUT", t)
testRouteNotOK("OPTIONS", t)
testRouteNotOK("HEAD", t)
}
// TestSingleRouteOK tests that POST route is correctly invoked.
func testRouteNotOK2(method string, t *testing.T) {
// SETUP
passed := false
r := New()
var methodRoute string
if method == "POST" {
methodRoute = "GET"
} else {
methodRoute = "POST"
}
r.Handle(methodRoute, "/test", []HandlerFunc{func(c *Context) {
passed = true
}})
// RUN
w := PerformRequest(r, method, "/test")
// TEST
if passed == true {
t.Errorf(method + " route handler was invoked, when it should not")
}
if w.Code != http.StatusMethodNotAllowed {
t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusMethodNotAllowed, w.Code, w.HeaderMap.Get("Location"))
}
}
// TestSingleRouteOK tests that POST route is correctly invoked.
func TestRouteNotOK2(t *testing.T) {
testRouteNotOK2("POST", t)
testRouteNotOK2("DELETE", t)
testRouteNotOK2("PATCH", t)
testRouteNotOK2("PUT", t)
testRouteNotOK2("OPTIONS", t)
testRouteNotOK2("HEAD", t)
}
// TestHandleStaticFile - ensure the static file handles properly
func TestHandleStaticFile(t *testing.T) {
// SETUP file
testRoot, _ := os.Getwd()
f, err := ioutil.TempFile(testRoot, "")
if err != nil {
t.Error(err)
}
defer os.Remove(f.Name())
filePath := path.Join("/", path.Base(f.Name()))
f.WriteString("Gin Web Framework")
f.Close()
// SETUP gin
r := New()
r.Static("./", testRoot)
// RUN
w := PerformRequest(r, "GET", filePath)
// TEST
if w.Code != 200 {
t.Errorf("Response code should be 200, was: %d", w.Code)
}
if w.Body.String() != "Gin Web Framework" {
t.Errorf("Response should be test, was: %s", w.Body.String())
}
if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" {
t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type"))
}
}
// TestHandleStaticDir - ensure the root/sub dir handles properly
func TestHandleStaticDir(t *testing.T) {
// SETUP
r := New()
r.Static("/", "./")
// RUN
w := PerformRequest(r, "GET", "/")
// TEST
bodyAsString := w.Body.String()
if w.Code != 200 {
t.Errorf("Response code should be 200, was: %d", w.Code)
}
if len(bodyAsString) == 0 {
t.Errorf("Got empty body instead of file tree")
}
if !strings.Contains(bodyAsString, "gin.go") {
t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString)
}
if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" {
t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type"))
}
}
// TestHandleHeadToDir - ensure the root/sub dir handles properly
func TestHandleHeadToDir(t *testing.T) {
// SETUP
r := New()
r.Static("/", "./")
// RUN
w := PerformRequest(r, "HEAD", "/")
// TEST
bodyAsString := w.Body.String()
if w.Code != 200 {
t.Errorf("Response code should be Ok, was: %s", w.Code)
}
if len(bodyAsString) == 0 {
t.Errorf("Got empty body instead of file tree")
}
if !strings.Contains(bodyAsString, "gin.go") {
t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString)
}
if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" {
t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type"))
}
}
-105
View File
@@ -1,105 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"log"
"os"
"time"
)
var (
green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109})
yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109})
red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})
blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109})
magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109})
cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109})
reset = string([]byte{27, 91, 48, 109})
)
func ErrorLogger() HandlerFunc {
return ErrorLoggerT(ErrorTypeAll)
}
func ErrorLoggerT(typ uint32) HandlerFunc {
return func(c *Context) {
c.Next()
errs := c.Errors.ByType(typ)
if len(errs) > 0 {
// -1 status code = do not change current one
c.JSON(-1, c.Errors)
}
}
}
func Logger() HandlerFunc {
stdlogger := log.New(os.Stdout, "", 0)
//errlogger := log.New(os.Stderr, "", 0)
return func(c *Context) {
// Start timer
start := time.Now()
// Process request
c.Next()
// Stop timer
end := time.Now()
latency := end.Sub(start)
clientIP := c.ClientIP()
method := c.Request.Method
statusCode := c.Writer.Status()
statusColor := colorForStatus(statusCode)
methodColor := colorForMethod(method)
stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %s |%s %s %-7s %s\n%s",
end.Format("2006/01/02 - 15:04:05"),
statusColor, statusCode, reset,
latency,
clientIP,
methodColor, reset, method,
c.Request.URL.Path,
c.Errors.String(),
)
}
}
func colorForStatus(code int) string {
switch {
case code >= 200 && code <= 299:
return green
case code >= 300 && code <= 399:
return white
case code >= 400 && code <= 499:
return yellow
default:
return red
}
}
func colorForMethod(method string) string {
switch {
case method == "GET":
return blue
case method == "POST":
return cyan
case method == "PUT":
return yellow
case method == "DELETE":
return red
case method == "PATCH":
return green
case method == "HEAD":
return magenta
case method == "OPTIONS":
return white
default:
return reset
}
}
-63
View File
@@ -1,63 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"fmt"
"os"
)
const GIN_MODE = "GIN_MODE"
const (
DebugMode string = "debug"
ReleaseMode string = "release"
TestMode string = "test"
)
const (
debugCode = iota
releaseCode = iota
testCode = iota
)
var gin_mode int = debugCode
var mode_name string = DebugMode
func init() {
value := os.Getenv(GIN_MODE)
if len(value) == 0 {
SetMode(DebugMode)
} else {
SetMode(value)
}
}
func SetMode(value string) {
switch value {
case DebugMode:
gin_mode = debugCode
case ReleaseMode:
gin_mode = releaseCode
case TestMode:
gin_mode = testCode
default:
panic("gin mode unknown: " + value)
}
mode_name = value
}
func Mode() string {
return mode_name
}
func IsDebugging() bool {
return gin_mode == debugCode
}
func debugPrint(format string, values ...interface{}) {
if IsDebugging() {
fmt.Printf("[GIN-debug] "+format, values...)
}
}
-98
View File
@@ -1,98 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"net/http"
"runtime"
)
var (
dunno = []byte("???")
centerDot = []byte("·")
dot = []byte(".")
slash = []byte("/")
)
// stack returns a nicely formated stack frame, skipping skip frames
func stack(skip int) []byte {
buf := new(bytes.Buffer) // the returned data
// As we loop, we open files and read them. These variables record the currently
// loaded file.
var lines [][]byte
var lastFile string
for i := skip; ; i++ { // Skip the expected number of frames
pc, file, line, ok := runtime.Caller(i)
if !ok {
break
}
// Print this much at least. If we can't find the source, it won't show.
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
if file != lastFile {
data, err := ioutil.ReadFile(file)
if err != nil {
continue
}
lines = bytes.Split(data, []byte{'\n'})
lastFile = file
}
fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
}
return buf.Bytes()
}
// source returns a space-trimmed slice of the n'th line.
func source(lines [][]byte, n int) []byte {
n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
if n < 0 || n >= len(lines) {
return dunno
}
return bytes.TrimSpace(lines[n])
}
// function returns, if possible, the name of the function containing the PC.
func function(pc uintptr) []byte {
fn := runtime.FuncForPC(pc)
if fn == nil {
return dunno
}
name := []byte(fn.Name())
// The name includes the path name to the package, which is unnecessary
// since the file name is already included. Plus, it has center dots.
// That is, we see
// runtime/debug.*T·ptrmethod
// and want
// *T.ptrmethod
// Also the package path might contains dot (e.g. code.google.com/...),
// so first eliminate the path prefix
if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 {
name = name[lastslash+1:]
}
if period := bytes.Index(name, dot); period >= 0 {
name = name[period+1:]
}
name = bytes.Replace(name, centerDot, dot, -1)
return name
}
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
// While Martini is in development mode, Recovery will also output the panic as HTML.
func Recovery() HandlerFunc {
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
stack := stack(3)
log.Printf("PANIC: %s\n%s", err, stack)
c.Writer.WriteHeader(http.StatusInternalServerError)
}
}()
c.Next()
}
}
-56
View File
@@ -1,56 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"bytes"
"log"
"os"
"testing"
)
// TestPanicInHandler assert that panic has been recovered.
func TestPanicInHandler(t *testing.T) {
// SETUP
log.SetOutput(bytes.NewBuffer(nil)) // Disable panic logs for testing
r := New()
r.Use(Recovery())
r.GET("/recovery", func(_ *Context) {
panic("Oupps, Houston, we have a problem")
})
// RUN
w := PerformRequest(r, "GET", "/recovery")
// restore logging
log.SetOutput(os.Stderr)
if w.Code != 500 {
t.Errorf("Response code should be Internal Server Error, was: %s", w.Code)
}
}
// TestPanicWithAbort assert that panic has been recovered even if context.Abort was used.
func TestPanicWithAbort(t *testing.T) {
// SETUP
log.SetOutput(bytes.NewBuffer(nil))
r := New()
r.Use(Recovery())
r.GET("/recovery", func(c *Context) {
c.AbortWithStatus(400)
panic("Oupps, Houston, we have a problem")
})
// RUN
w := PerformRequest(r, "GET", "/recovery")
// restore logging
log.SetOutput(os.Stderr)
// TEST
if w.Code != 500 {
t.Errorf("Response code should be Bad request, was: %s", w.Code)
}
}
-123
View File
@@ -1,123 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package render
import (
"encoding/json"
"encoding/xml"
"fmt"
"html/template"
"net/http"
)
type (
Render interface {
Render(http.ResponseWriter, int, ...interface{}) error
}
// JSON binding
jsonRender struct{}
// XML binding
xmlRender struct{}
// Plain text
plainRender struct{}
// Redirects
redirectRender struct{}
// Redirects
htmlDebugRender struct {
files []string
globs []string
}
// form binding
HTMLRender struct {
Template *template.Template
}
)
var (
JSON = jsonRender{}
XML = xmlRender{}
Plain = plainRender{}
Redirect = redirectRender{}
HTMLDebug = &htmlDebugRender{}
)
func writeHeader(w http.ResponseWriter, code int, contentType string) {
w.Header().Set("Content-Type", contentType+"; charset=utf-8")
w.WriteHeader(code)
}
func (_ jsonRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
writeHeader(w, code, "application/json")
encoder := json.NewEncoder(w)
return encoder.Encode(data[0])
}
func (_ redirectRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
w.Header().Set("Location", data[0].(string))
w.WriteHeader(code)
return nil
}
func (_ xmlRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
writeHeader(w, code, "application/xml")
encoder := xml.NewEncoder(w)
return encoder.Encode(data[0])
}
func (_ plainRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
writeHeader(w, code, "text/plain")
format := data[0].(string)
args := data[1].([]interface{})
var err error
if len(args) > 0 {
_, err = w.Write([]byte(fmt.Sprintf(format, args...)))
} else {
_, err = w.Write([]byte(format))
}
return err
}
func (r *htmlDebugRender) AddGlob(pattern string) {
r.globs = append(r.globs, pattern)
}
func (r *htmlDebugRender) AddFiles(files ...string) {
r.files = append(r.files, files...)
}
func (r *htmlDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
writeHeader(w, code, "text/html")
file := data[0].(string)
obj := data[1]
t := template.New("")
if len(r.files) > 0 {
if _, err := t.ParseFiles(r.files...); err != nil {
return err
}
}
for _, glob := range r.globs {
if _, err := t.ParseGlob(glob); err != nil {
return err
}
}
return t.ExecuteTemplate(w, file, obj)
}
func (html HTMLRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
writeHeader(w, code, "text/html")
file := data[0].(string)
obj := data[1]
return html.Template.ExecuteTemplate(w, file, obj)
}
-100
View File
@@ -1,100 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"bufio"
"errors"
"log"
"net"
"net/http"
)
const (
NoWritten = -1
)
type (
ResponseWriter interface {
http.ResponseWriter
http.Hijacker
http.Flusher
http.CloseNotifier
Status() int
Size() int
Written() bool
WriteHeaderNow()
}
responseWriter struct {
http.ResponseWriter
status int
size int
}
)
func (w *responseWriter) reset(writer http.ResponseWriter) {
w.ResponseWriter = writer
w.status = 200
w.size = NoWritten
}
func (w *responseWriter) WriteHeader(code int) {
if code > 0 {
w.status = code
if w.Written() {
log.Println("[GIN] WARNING. Headers were already written!")
}
}
}
func (w *responseWriter) WriteHeaderNow() {
if !w.Written() {
w.size = 0
w.ResponseWriter.WriteHeader(w.status)
}
}
func (w *responseWriter) Write(data []byte) (n int, err error) {
w.WriteHeaderNow()
n, err = w.ResponseWriter.Write(data)
w.size += n
return
}
func (w *responseWriter) Status() int {
return w.status
}
func (w *responseWriter) Size() int {
return w.size
}
func (w *responseWriter) Written() bool {
return w.size != NoWritten
}
// Implements the http.Hijacker interface
func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
hijacker, ok := w.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, errors.New("the ResponseWriter doesn't support the Hijacker interface")
}
return hijacker.Hijack()
}
// Implements the http.CloseNotify interface
func (w *responseWriter) CloseNotify() <-chan bool {
return w.ResponseWriter.(http.CloseNotifier).CloseNotify()
}
// Implements the http.Flush interface
func (w *responseWriter) Flush() {
flusher, ok := w.ResponseWriter.(http.Flusher)
if ok {
flusher.Flush()
}
}
-148
View File
@@ -1,148 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"github.com/julienschmidt/httprouter"
"net/http"
"path"
)
// Used internally to configure router, a RouterGroup is associated with a prefix
// and an array of handlers (middlewares)
type RouterGroup struct {
Handlers []HandlerFunc
absolutePath string
engine *Engine
}
// Adds middlewares to the group, see example code in github.
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
group.Handlers = append(group.Handlers, middlewares...)
}
// Creates a new router group. You should add all the routes that have common middlwares or the same path prefix.
// For example, all the routes that use a common middlware for authorization could be grouped.
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
return &RouterGroup{
Handlers: group.combineHandlers(handlers),
absolutePath: group.calculateAbsolutePath(relativePath),
engine: group.engine,
}
}
// Handle registers a new request handle and middlewares with the given path and method.
// The last handler should be the real handler, the other ones should be middlewares that can and should be shared among different routes.
// See the example code in github.
//
// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
// functions can be used.
//
// This function is intended for bulk loading and to allow the usage of less
// frequently used, non-standardized or custom methods (e.g. for internal
// communication with a proxy).
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers []HandlerFunc) {
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers)
if IsDebugging() {
nuHandlers := len(handlers)
handlerName := nameOfFunction(handlers[nuHandlers-1])
debugPrint("%-5s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers)
}
group.engine.router.Handle(httpMethod, absolutePath, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
context := group.engine.createContext(w, req, params, handlers)
context.Next()
context.Writer.WriteHeaderNow()
group.engine.reuseContext(context)
})
}
// POST is a shortcut for router.Handle("POST", path, handle)
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) {
group.Handle("POST", relativePath, handlers)
}
// GET is a shortcut for router.Handle("GET", path, handle)
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) {
group.Handle("GET", relativePath, handlers)
}
// DELETE is a shortcut for router.Handle("DELETE", path, handle)
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) {
group.Handle("DELETE", relativePath, handlers)
}
// PATCH is a shortcut for router.Handle("PATCH", path, handle)
func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) {
group.Handle("PATCH", relativePath, handlers)
}
// PUT is a shortcut for router.Handle("PUT", path, handle)
func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) {
group.Handle("PUT", relativePath, handlers)
}
// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle)
func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) {
group.Handle("OPTIONS", relativePath, handlers)
}
// HEAD is a shortcut for router.Handle("HEAD", path, handle)
func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) {
group.Handle("HEAD", relativePath, handlers)
}
// LINK is a shortcut for router.Handle("LINK", path, handle)
func (group *RouterGroup) LINK(relativePath string, handlers ...HandlerFunc) {
group.Handle("LINK", relativePath, handlers)
}
// UNLINK is a shortcut for router.Handle("UNLINK", path, handle)
func (group *RouterGroup) UNLINK(relativePath string, handlers ...HandlerFunc) {
group.Handle("UNLINK", relativePath, handlers)
}
// Static serves files from the given file system root.
// Internally a http.FileServer is used, therefore http.NotFound is used instead
// of the Router's NotFound handler.
// To use the operating system's file system implementation,
// use :
// router.Static("/static", "/var/www")
func (group *RouterGroup) Static(relativePath, root string) {
absolutePath := group.calculateAbsolutePath(relativePath)
handler := group.createStaticHandler(absolutePath, root)
absolutePath = path.Join(absolutePath, "/*filepath")
// Register GET and HEAD handlers
group.GET(absolutePath, handler)
group.HEAD(absolutePath, handler)
}
func (group *RouterGroup) createStaticHandler(absolutePath, root string) func(*Context) {
fileServer := http.StripPrefix(absolutePath, http.FileServer(http.Dir(root)))
return func(c *Context) {
fileServer.ServeHTTP(c.Writer, c.Request)
}
}
func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc {
finalSize := len(group.Handlers) + len(handlers)
mergedHandlers := make([]HandlerFunc, 0, finalSize)
mergedHandlers = append(mergedHandlers, group.Handlers...)
return append(mergedHandlers, handlers...)
}
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
if len(relativePath) == 0 {
return group.absolutePath
}
absolutePath := path.Join(group.absolutePath, relativePath)
appendSlash := lastChar(relativePath) == '/' && lastChar(absolutePath) != '/'
if appendSlash {
return absolutePath + "/"
}
return absolutePath
}
-82
View File
@@ -1,82 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"encoding/xml"
"reflect"
"runtime"
"strings"
)
type H map[string]interface{}
// Allows type H to be used with xml.Marshal
func (h H) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
start.Name = xml.Name{
Space: "",
Local: "map",
}
if err := e.EncodeToken(start); err != nil {
return err
}
for key, value := range h {
elem := xml.StartElement{
Name: xml.Name{Space: "", Local: key},
Attr: []xml.Attr{},
}
if err := e.EncodeElement(value, elem); err != nil {
return err
}
}
if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil {
return err
}
return nil
}
func filterFlags(content string) string {
for i, char := range content {
if char == ' ' || char == ';' {
return content[:i]
}
}
return content
}
func chooseData(custom, wildcard interface{}) interface{} {
if custom == nil {
if wildcard == nil {
panic("negotiation config is invalid")
}
return wildcard
}
return custom
}
func parseAccept(accept string) []string {
parts := strings.Split(accept, ",")
for i, part := range parts {
index := strings.IndexByte(part, ';')
if index >= 0 {
part = part[0:index]
}
part = strings.TrimSpace(part)
parts[i] = part
}
return parts
}
func lastChar(str string) uint8 {
size := len(str)
if size == 0 {
panic("The length of the string can't be 0")
}
return str[size-1]
}
func nameOfFunction(f interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
}
-35
View File
@@ -1,35 +0,0 @@
language: go
install:
# go-flags
- go get -d -v ./...
- go build -v ./...
# linting
- go get code.google.com/p/go.tools/cmd/vet
- go get github.com/golang/lint
- go install github.com/golang/lint/golint
# code coverage
- go get code.google.com/p/go.tools/cmd/cover
- go get github.com/onsi/ginkgo/ginkgo
- go get github.com/modocache/gover
- if [ "$TRAVIS_SECURE_ENV_VARS" = "true" ]; then go get github.com/mattn/goveralls; fi
script:
# go-flags
- $(exit $(gofmt -l . | wc -l))
- go test -v ./...
# linting
- go tool vet -all=true -v=true . || true
- $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/golint ./...
# code coverage
- $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/ginkgo -r -cover
- $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/gover
- if [ "$TRAVIS_SECURE_ENV_VARS" = "true" ]; then $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/goveralls -coverprofile=gover.coverprofile -service=travis-ci -repotoken $COVERALLS_TOKEN; fi
env:
# coveralls.io
secure: "RCYbiB4P0RjQRIoUx/vG/AjP3mmYCbzOmr86DCww1Z88yNcy3hYr3Cq8rpPtYU5v0g7wTpu4adaKIcqRE9xknYGbqj3YWZiCoBP1/n4Z+9sHW3Dsd9D/GRGeHUus0laJUGARjWoCTvoEtOgTdGQDoX7mH+pUUY0FBltNYUdOiiU="
-26
View File
@@ -1,26 +0,0 @@
Copyright (c) 2012 Jesse van den Kieboom. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-131
View File
@@ -1,131 +0,0 @@
go-flags: a go library for parsing command line arguments
=========================================================
[![GoDoc](https://godoc.org/github.com/jessevdk/go-flags?status.png)](https://godoc.org/github.com/jessevdk/go-flags) [![Build Status](https://travis-ci.org/jessevdk/go-flags.svg?branch=master)](https://travis-ci.org/jessevdk/go-flags) [![Coverage Status](https://img.shields.io/coveralls/jessevdk/go-flags.svg)](https://coveralls.io/r/jessevdk/go-flags?branch=master)
This library provides similar functionality to the builtin flag library of
go, but provides much more functionality and nicer formatting. From the
documentation:
Package flags provides an extensive command line option parser.
The flags package is similar in functionality to the go builtin flag package
but provides more options and uses reflection to provide a convenient and
succinct way of specifying command line options.
Supported features:
* Options with short names (-v)
* Options with long names (--verbose)
* Options with and without arguments (bool v.s. other type)
* Options with optional arguments and default values
* Multiple option groups each containing a set of options
* Generate and print well-formatted help message
* Passing remaining command line arguments after -- (optional)
* Ignoring unknown command line options (optional)
* Supports -I/usr/include -I=/usr/include -I /usr/include option argument specification
* Supports multiple short options -aux
* Supports all primitive go types (string, int{8..64}, uint{8..64}, float)
* Supports same option multiple times (can store in slice or last option counts)
* Supports maps
* Supports function callbacks
* Supports namespaces for (nested) option groups
The flags package uses structs, reflection and struct field tags
to allow users to specify command line options. This results in very simple
and concise specification of your application options. For example:
type Options struct {
Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"`
}
This specifies one option with a short name -v and a long name --verbose.
When either -v or --verbose is found on the command line, a 'true' value
will be appended to the Verbose field. e.g. when specifying -vvv, the
resulting value of Verbose will be {[true, true, true]}.
Example:
--------
var opts struct {
// Slice of bool will append 'true' each time the option
// is encountered (can be set multiple times, like -vvv)
Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"`
// Example of automatic marshalling to desired type (uint)
Offset uint `long:"offset" description:"Offset"`
// Example of a callback, called each time the option is found.
Call func(string) `short:"c" description:"Call phone number"`
// Example of a required flag
Name string `short:"n" long:"name" description:"A name" required:"true"`
// Example of a value name
File string `short:"f" long:"file" description:"A file" value-name:"FILE"`
// Example of a pointer
Ptr *int `short:"p" description:"A pointer to an integer"`
// Example of a slice of strings
StringSlice []string `short:"s" description:"A slice of strings"`
// Example of a slice of pointers
PtrSlice []*string `long:"ptrslice" description:"A slice of pointers to string"`
// Example of a map
IntMap map[string]int `long:"intmap" description:"A map from string to int"`
}
// Callback which will invoke callto:<argument> to call a number.
// Note that this works just on OS X (and probably only with
// Skype) but it shows the idea.
opts.Call = func(num string) {
cmd := exec.Command("open", "callto:"+num)
cmd.Start()
cmd.Process.Release()
}
// Make some fake arguments to parse.
args := []string{
"-vv",
"--offset=5",
"-n", "Me",
"-p", "3",
"-s", "hello",
"-s", "world",
"--ptrslice", "hello",
"--ptrslice", "world",
"--intmap", "a:1",
"--intmap", "b:5",
"arg1",
"arg2",
"arg3",
}
// Parse flags from `args'. Note that here we use flags.ParseArgs for
// the sake of making a working example. Normally, you would simply use
// flags.Parse(&opts) which uses os.Args
args, err := flags.ParseArgs(&opts, args)
if err != nil {
panic(err)
os.Exit(1)
}
fmt.Printf("Verbosity: %v\n", opts.Verbose)
fmt.Printf("Offset: %d\n", opts.Offset)
fmt.Printf("Name: %s\n", opts.Name)
fmt.Printf("Ptr: %d\n", *opts.Ptr)
fmt.Printf("StringSlice: %v\n", opts.StringSlice)
fmt.Printf("PtrSlice: [%v %v]\n", *opts.PtrSlice[0], *opts.PtrSlice[1])
fmt.Printf("IntMap: [a:%v b:%v]\n", opts.IntMap["a"], opts.IntMap["b"])
fmt.Printf("Remaining args: %s\n", strings.Join(args, " "))
// Output: Verbosity: [true true]
// Offset: 5
// Name: Me
// Ptr: 3
// StringSlice: [hello world]
// PtrSlice: [hello world]
// IntMap: [a:1 b:5]
// Remaining args: arg1 arg2 arg3
More information can be found in the godocs: <http://godoc.org/github.com/jessevdk/go-flags>
-21
View File
@@ -1,21 +0,0 @@
package flags
import (
"reflect"
)
// Arg represents a positional argument on the command line.
type Arg struct {
// The name of the positional argument (used in the help)
Name string
// A description of the positional argument (used in the help)
Description string
value reflect.Value
tag multiTag
}
func (a *Arg) isRemaining() bool {
return a.value.Type().Kind() == reflect.Slice
}
-53
View File
@@ -1,53 +0,0 @@
package flags
import (
"testing"
)
func TestPositional(t *testing.T) {
var opts = struct {
Value bool `short:"v"`
Positional struct {
Command int
Filename string
Rest []string
} `positional-args:"yes" required:"yes"`
}{}
p := NewParser(&opts, Default)
ret, err := p.ParseArgs([]string{"10", "arg_test.go", "a", "b"})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
return
}
if opts.Positional.Command != 10 {
t.Fatalf("Expected opts.Positional.Command to be 10, but got %v", opts.Positional.Command)
}
if opts.Positional.Filename != "arg_test.go" {
t.Fatalf("Expected opts.Positional.Filename to be \"arg_test.go\", but got %v", opts.Positional.Filename)
}
assertStringArray(t, opts.Positional.Rest, []string{"a", "b"})
assertStringArray(t, ret, []string{})
}
func TestPositionalRequired(t *testing.T) {
var opts = struct {
Value bool `short:"v"`
Positional struct {
Command int
Filename string
Rest []string
} `positional-args:"yes" required:"yes"`
}{}
p := NewParser(&opts, None)
_, err := p.ParseArgs([]string{"10"})
assertError(t, err, ErrRequired, "the required argument `Filename` was not provided")
}
-177
View File
@@ -1,177 +0,0 @@
package flags
import (
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path"
"runtime"
"testing"
)
func assertCallerInfo() (string, int) {
ptr := make([]uintptr, 15)
n := runtime.Callers(1, ptr)
if n == 0 {
return "", 0
}
mef := runtime.FuncForPC(ptr[0])
mefile, meline := mef.FileLine(ptr[0])
for i := 2; i < n; i++ {
f := runtime.FuncForPC(ptr[i])
file, line := f.FileLine(ptr[i])
if file != mefile {
return file, line
}
}
return mefile, meline
}
func assertErrorf(t *testing.T, format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
file, line := assertCallerInfo()
t.Errorf("%s:%d: %s", path.Base(file), line, msg)
}
func assertFatalf(t *testing.T, format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
file, line := assertCallerInfo()
t.Fatalf("%s:%d: %s", path.Base(file), line, msg)
}
func assertString(t *testing.T, a string, b string) {
if a != b {
assertErrorf(t, "Expected %#v, but got %#v", b, a)
}
}
func assertStringArray(t *testing.T, a []string, b []string) {
if len(a) != len(b) {
assertErrorf(t, "Expected %#v, but got %#v", b, a)
return
}
for i, v := range a {
if b[i] != v {
assertErrorf(t, "Expected %#v, but got %#v", b, a)
return
}
}
}
func assertBoolArray(t *testing.T, a []bool, b []bool) {
if len(a) != len(b) {
assertErrorf(t, "Expected %#v, but got %#v", b, a)
return
}
for i, v := range a {
if b[i] != v {
assertErrorf(t, "Expected %#v, but got %#v", b, a)
return
}
}
}
func assertParserSuccess(t *testing.T, data interface{}, args ...string) (*Parser, []string) {
parser := NewParser(data, Default&^PrintErrors)
ret, err := parser.ParseArgs(args)
if err != nil {
t.Fatalf("Unexpected parse error: %s", err)
return nil, nil
}
return parser, ret
}
func assertParseSuccess(t *testing.T, data interface{}, args ...string) []string {
_, ret := assertParserSuccess(t, data, args...)
return ret
}
func assertError(t *testing.T, err error, typ ErrorType, msg string) {
if err == nil {
assertFatalf(t, "Expected error: %s", msg)
return
}
if e, ok := err.(*Error); !ok {
assertFatalf(t, "Expected Error type, but got %#v", err)
} else {
if e.Type != typ {
assertErrorf(t, "Expected error type {%s}, but got {%s}", typ, e.Type)
}
if e.Message != msg {
assertErrorf(t, "Expected error message %#v, but got %#v", msg, e.Message)
}
}
}
func assertParseFail(t *testing.T, typ ErrorType, msg string, data interface{}, args ...string) []string {
parser := NewParser(data, Default&^PrintErrors)
ret, err := parser.ParseArgs(args)
assertError(t, err, typ, msg)
return ret
}
func diff(a, b string) (string, error) {
atmp, err := ioutil.TempFile("", "help-diff")
if err != nil {
return "", err
}
btmp, err := ioutil.TempFile("", "help-diff")
if err != nil {
return "", err
}
if _, err := io.WriteString(atmp, a); err != nil {
return "", err
}
if _, err := io.WriteString(btmp, b); err != nil {
return "", err
}
ret, err := exec.Command("diff", "-u", "-d", "--label", "got", atmp.Name(), "--label", "expected", btmp.Name()).Output()
os.Remove(atmp.Name())
os.Remove(btmp.Name())
if err.Error() == "exit status 1" {
return string(ret), nil
}
return string(ret), err
}
func assertDiff(t *testing.T, actual, expected, msg string) {
if actual == expected {
return
}
ret, err := diff(actual, expected)
if err != nil {
assertErrorf(t, "Unexpected diff error: %s", err)
assertErrorf(t, "Unexpected %s, expected:\n\n%s\n\nbut got\n\n%s", msg, expected, actual)
} else {
assertErrorf(t, "Unexpected %s:\n\n%s", msg, ret)
}
}
@@ -1,16 +0,0 @@
#!/bin/bash
set -e
echo '# linux arm7'
GOARM=7 GOARCH=arm GOOS=linux go build
echo '# linux arm5'
GOARM=5 GOARCH=arm GOOS=linux go build
echo '# windows 386'
GOARCH=386 GOOS=windows go build
echo '# windows amd64'
GOARCH=amd64 GOOS=windows go build
echo '# darwin'
GOARCH=amd64 GOOS=darwin go build
echo '# freebsd'
GOARCH=amd64 GOOS=freebsd go build
-59
View File
@@ -1,59 +0,0 @@
package flags
func levenshtein(s string, t string) int {
if len(s) == 0 {
return len(t)
}
if len(t) == 0 {
return len(s)
}
dists := make([][]int, len(s)+1)
for i := range dists {
dists[i] = make([]int, len(t)+1)
dists[i][0] = i
}
for j := range t {
dists[0][j] = j
}
for i, sc := range s {
for j, tc := range t {
if sc == tc {
dists[i+1][j+1] = dists[i][j]
} else {
dists[i+1][j+1] = dists[i][j] + 1
if dists[i+1][j] < dists[i+1][j+1] {
dists[i+1][j+1] = dists[i+1][j] + 1
}
if dists[i][j+1] < dists[i+1][j+1] {
dists[i+1][j+1] = dists[i][j+1] + 1
}
}
}
}
return dists[len(s)][len(t)]
}
func closestChoice(cmd string, choices []string) (string, int) {
if len(choices) == 0 {
return "", 0
}
mincmd := -1
mindist := -1
for i, c := range choices {
l := levenshtein(cmd, c)
if mincmd < 0 || l < mindist {
mindist = l
mincmd = i
}
}
return choices[mincmd], mindist
}
-106
View File
@@ -1,106 +0,0 @@
package flags
// Command represents an application command. Commands can be added to the
// parser (which itself is a command) and are selected/executed when its name
// is specified on the command line. The Command type embeds a Group and
// therefore also carries a set of command specific options.
type Command struct {
// Embedded, see Group for more information
*Group
// The name by which the command can be invoked
Name string
// The active sub command (set by parsing) or nil
Active *Command
// Whether subcommands are optional
SubcommandsOptional bool
// Aliases for the command
Aliases []string
// Whether positional arguments are required
ArgsRequired bool
commands []*Command
hasBuiltinHelpGroup bool
args []*Arg
}
// Commander is an interface which can be implemented by any command added in
// the options. When implemented, the Execute method will be called for the last
// specified (sub)command providing the remaining command line arguments.
type Commander interface {
// Execute will be called for the last active (sub)command. The
// args argument contains the remaining command line arguments. The
// error that Execute returns will be eventually passed out of the
// Parse method of the Parser.
Execute(args []string) error
}
// Usage is an interface which can be implemented to show a custom usage string
// in the help message shown for a command.
type Usage interface {
// Usage is called for commands to allow customized printing of command
// usage in the generated help message.
Usage() string
}
// AddCommand adds a new command to the parser with the given name and data. The
// data needs to be a pointer to a struct from which the fields indicate which
// options are in the command. The provided data can implement the Command and
// Usage interfaces.
func (c *Command) AddCommand(command string, shortDescription string, longDescription string, data interface{}) (*Command, error) {
cmd := newCommand(command, shortDescription, longDescription, data)
cmd.parent = c
if err := cmd.scan(); err != nil {
return nil, err
}
c.commands = append(c.commands, cmd)
return cmd, nil
}
// AddGroup adds a new group to the command with the given name and data. The
// data needs to be a pointer to a struct from which the fields indicate which
// options are in the group.
func (c *Command) AddGroup(shortDescription string, longDescription string, data interface{}) (*Group, error) {
group := newGroup(shortDescription, longDescription, data)
group.parent = c
if err := group.scanType(c.scanSubcommandHandler(group)); err != nil {
return nil, err
}
c.groups = append(c.groups, group)
return group, nil
}
// Commands returns a list of subcommands of this command.
func (c *Command) Commands() []*Command {
return c.commands
}
// Find locates the subcommand with the given name and returns it. If no such
// command can be found Find will return nil.
func (c *Command) Find(name string) *Command {
for _, cc := range c.commands {
if cc.match(name) {
return cc
}
}
return nil
}
// Args returns a list of positional arguments associated with this command.
func (c *Command) Args() []*Arg {
ret := make([]*Arg, len(c.args))
copy(ret, c.args)
return ret
}
-250
View File
@@ -1,250 +0,0 @@
package flags
import (
"reflect"
"sort"
"strings"
"unsafe"
)
type lookup struct {
shortNames map[string]*Option
longNames map[string]*Option
commands map[string]*Command
}
func newCommand(name string, shortDescription string, longDescription string, data interface{}) *Command {
return &Command{
Group: newGroup(shortDescription, longDescription, data),
Name: name,
}
}
func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler {
f := func(realval reflect.Value, sfield *reflect.StructField) (bool, error) {
mtag := newMultiTag(string(sfield.Tag))
if err := mtag.Parse(); err != nil {
return true, err
}
positional := mtag.Get("positional-args")
if len(positional) != 0 {
stype := realval.Type()
for i := 0; i < stype.NumField(); i++ {
field := stype.Field(i)
m := newMultiTag((string(field.Tag)))
if err := m.Parse(); err != nil {
return true, err
}
name := m.Get("name")
if len(name) == 0 {
name = field.Name
}
arg := &Arg{
Name: name,
Description: m.Get("description"),
value: realval.Field(i),
tag: m,
}
c.args = append(c.args, arg)
if len(mtag.Get("required")) != 0 {
c.ArgsRequired = true
}
}
return true, nil
}
subcommand := mtag.Get("command")
if len(subcommand) != 0 {
ptrval := reflect.NewAt(realval.Type(), unsafe.Pointer(realval.UnsafeAddr()))
shortDescription := mtag.Get("description")
longDescription := mtag.Get("long-description")
subcommandsOptional := mtag.Get("subcommands-optional")
aliases := mtag.GetMany("alias")
subc, err := c.AddCommand(subcommand, shortDescription, longDescription, ptrval.Interface())
if err != nil {
return true, err
}
if len(subcommandsOptional) > 0 {
subc.SubcommandsOptional = true
}
if len(aliases) > 0 {
subc.Aliases = aliases
}
return true, nil
}
return parentg.scanSubGroupHandler(realval, sfield)
}
return f
}
func (c *Command) scan() error {
return c.scanType(c.scanSubcommandHandler(c.Group))
}
func (c *Command) eachCommand(f func(*Command), recurse bool) {
f(c)
for _, cc := range c.commands {
if recurse {
cc.eachCommand(f, true)
} else {
f(cc)
}
}
}
func (c *Command) eachActiveGroup(f func(cc *Command, g *Group)) {
c.eachGroup(func(g *Group) {
f(c, g)
})
if c.Active != nil {
c.Active.eachActiveGroup(f)
}
}
func (c *Command) addHelpGroups(showHelp func() error) {
if !c.hasBuiltinHelpGroup {
c.addHelpGroup(showHelp)
c.hasBuiltinHelpGroup = true
}
for _, cc := range c.commands {
cc.addHelpGroups(showHelp)
}
}
func (c *Command) makeLookup() lookup {
ret := lookup{
shortNames: make(map[string]*Option),
longNames: make(map[string]*Option),
commands: make(map[string]*Command),
}
c.eachGroup(func(g *Group) {
for _, option := range g.options {
if option.ShortName != 0 {
ret.shortNames[string(option.ShortName)] = option
}
if len(option.LongName) > 0 {
ret.longNames[option.LongNameWithNamespace()] = option
}
}
})
for _, subcommand := range c.commands {
ret.commands[subcommand.Name] = subcommand
for _, a := range subcommand.Aliases {
ret.commands[a] = subcommand
}
}
return ret
}
func (c *Command) groupByName(name string) *Group {
if grp := c.Group.groupByName(name); grp != nil {
return grp
}
for _, subc := range c.commands {
prefix := subc.Name + "."
if strings.HasPrefix(name, prefix) {
if grp := subc.groupByName(name[len(prefix):]); grp != nil {
return grp
}
} else if name == subc.Name {
return subc.Group
}
}
return nil
}
type commandList []*Command
func (c commandList) Less(i, j int) bool {
return c[i].Name < c[j].Name
}
func (c commandList) Len() int {
return len(c)
}
func (c commandList) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
func (c *Command) sortedCommands() []*Command {
ret := make(commandList, len(c.commands))
copy(ret, c.commands)
sort.Sort(ret)
return []*Command(ret)
}
func (c *Command) match(name string) bool {
if c.Name == name {
return true
}
for _, v := range c.Aliases {
if v == name {
return true
}
}
return false
}
func (c *Command) hasCliOptions() bool {
ret := false
c.eachGroup(func(g *Group) {
if g.isBuiltinHelp {
return
}
for _, opt := range g.options {
if opt.canCli() {
ret = true
}
}
})
return ret
}
func (c *Command) fillParseState(s *parseState) {
s.positional = make([]*Arg, len(c.args))
copy(s.positional, c.args)
s.lookup = c.makeLookup()
s.command = c
}
-354
View File
@@ -1,354 +0,0 @@
package flags
import (
"fmt"
"testing"
)
func TestCommandInline(t *testing.T) {
var opts = struct {
Value bool `short:"v"`
Command struct {
G bool `short:"g"`
} `command:"cmd"`
}{}
p, ret := assertParserSuccess(t, &opts, "-v", "cmd", "-g")
assertStringArray(t, ret, []string{})
if p.Active == nil {
t.Errorf("Expected active command")
}
if !opts.Value {
t.Errorf("Expected Value to be true")
}
if !opts.Command.G {
t.Errorf("Expected Command.G to be true")
}
if p.Command.Find("cmd") != p.Active {
t.Errorf("Expected to find command `cmd' to be active")
}
}
func TestCommandInlineMulti(t *testing.T) {
var opts = struct {
Value bool `short:"v"`
C1 struct {
} `command:"c1"`
C2 struct {
G bool `short:"g"`
} `command:"c2"`
}{}
p, ret := assertParserSuccess(t, &opts, "-v", "c2", "-g")
assertStringArray(t, ret, []string{})
if p.Active == nil {
t.Errorf("Expected active command")
}
if !opts.Value {
t.Errorf("Expected Value to be true")
}
if !opts.C2.G {
t.Errorf("Expected C2.G to be true")
}
if p.Command.Find("c1") == nil {
t.Errorf("Expected to find command `c1'")
}
if c2 := p.Command.Find("c2"); c2 == nil {
t.Errorf("Expected to find command `c2'")
} else if c2 != p.Active {
t.Errorf("Expected to find command `c2' to be active")
}
}
func TestCommandFlagOrder1(t *testing.T) {
var opts = struct {
Value bool `short:"v"`
Command struct {
G bool `short:"g"`
} `command:"cmd"`
}{}
assertParseFail(t, ErrUnknownFlag, "unknown flag `g'", &opts, "-v", "-g", "cmd")
}
func TestCommandFlagOrder2(t *testing.T) {
var opts = struct {
Value bool `short:"v"`
Command struct {
G bool `short:"g"`
} `command:"cmd"`
}{}
assertParseFail(t, ErrUnknownFlag, "unknown flag `v'", &opts, "cmd", "-v", "-g")
}
func TestCommandEstimate(t *testing.T) {
var opts = struct {
Value bool `short:"v"`
Cmd1 struct {
} `command:"remove"`
Cmd2 struct {
} `command:"add"`
}{}
p := NewParser(&opts, None)
_, err := p.ParseArgs([]string{})
assertError(t, err, ErrCommandRequired, "Please specify one command of: add or remove")
}
func TestCommandEstimate2(t *testing.T) {
var opts = struct {
Value bool `short:"v"`
Cmd1 struct {
} `command:"remove"`
Cmd2 struct {
} `command:"add"`
}{}
p := NewParser(&opts, None)
_, err := p.ParseArgs([]string{"rmive"})
assertError(t, err, ErrUnknownCommand, "Unknown command `rmive', did you mean `remove'?")
}
type testCommand struct {
G bool `short:"g"`
Executed bool
EArgs []string
}
func (c *testCommand) Execute(args []string) error {
c.Executed = true
c.EArgs = args
return nil
}
func TestCommandExecute(t *testing.T) {
var opts = struct {
Value bool `short:"v"`
Command testCommand `command:"cmd"`
}{}
assertParseSuccess(t, &opts, "-v", "cmd", "-g", "a", "b")
if !opts.Value {
t.Errorf("Expected Value to be true")
}
if !opts.Command.Executed {
t.Errorf("Did not execute command")
}
if !opts.Command.G {
t.Errorf("Expected Command.C to be true")
}
assertStringArray(t, opts.Command.EArgs, []string{"a", "b"})
}
func TestCommandClosest(t *testing.T) {
var opts = struct {
Value bool `short:"v"`
Cmd1 struct {
} `command:"remove"`
Cmd2 struct {
} `command:"add"`
}{}
args := assertParseFail(t, ErrUnknownCommand, "Unknown command `addd', did you mean `add'?", &opts, "-v", "addd")
assertStringArray(t, args, []string{"addd"})
}
func TestCommandAdd(t *testing.T) {
var opts = struct {
Value bool `short:"v"`
}{}
var cmd = struct {
G bool `short:"g"`
}{}
p := NewParser(&opts, Default)
c, err := p.AddCommand("cmd", "", "", &cmd)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
return
}
ret, err := p.ParseArgs([]string{"-v", "cmd", "-g", "rest"})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
return
}
assertStringArray(t, ret, []string{"rest"})
if !opts.Value {
t.Errorf("Expected Value to be true")
}
if !cmd.G {
t.Errorf("Expected Command.G to be true")
}
if p.Command.Find("cmd") != c {
t.Errorf("Expected to find command `cmd'")
}
if p.Commands()[0] != c {
t.Errorf("Expected command %#v, but got %#v", c, p.Commands()[0])
}
if c.Options()[0].ShortName != 'g' {
t.Errorf("Expected short name `g' but got %v", c.Options()[0].ShortName)
}
}
func TestCommandNestedInline(t *testing.T) {
var opts = struct {
Value bool `short:"v"`
Command struct {
G bool `short:"g"`
Nested struct {
N string `long:"n"`
} `command:"nested"`
} `command:"cmd"`
}{}
p, ret := assertParserSuccess(t, &opts, "-v", "cmd", "-g", "nested", "--n", "n", "rest")
assertStringArray(t, ret, []string{"rest"})
if !opts.Value {
t.Errorf("Expected Value to be true")
}
if !opts.Command.G {
t.Errorf("Expected Command.G to be true")
}
assertString(t, opts.Command.Nested.N, "n")
if c := p.Command.Find("cmd"); c == nil {
t.Errorf("Expected to find command `cmd'")
} else {
if c != p.Active {
t.Errorf("Expected `cmd' to be the active parser command")
}
if nested := c.Find("nested"); nested == nil {
t.Errorf("Expected to find command `nested'")
} else if nested != c.Active {
t.Errorf("Expected to find command `nested' to be the active `cmd' command")
}
}
}
func TestRequiredOnCommand(t *testing.T) {
var opts = struct {
Value bool `short:"v" required:"true"`
Command struct {
G bool `short:"g"`
} `command:"cmd"`
}{}
assertParseFail(t, ErrRequired, fmt.Sprintf("the required flag `%cv' was not specified", defaultShortOptDelimiter), &opts, "cmd")
}
func TestRequiredAllOnCommand(t *testing.T) {
var opts = struct {
Value bool `short:"v" required:"true"`
Missing bool `long:"missing" required:"true"`
Command struct {
G bool `short:"g"`
} `command:"cmd"`
}{}
assertParseFail(t, ErrRequired, fmt.Sprintf("the required flags `%smissing' and `%cv' were not specified", defaultLongOptDelimiter, defaultShortOptDelimiter), &opts, "cmd")
}
func TestDefaultOnCommand(t *testing.T) {
var opts = struct {
Command struct {
G bool `short:"g" default:"true"`
} `command:"cmd"`
}{}
assertParseSuccess(t, &opts, "cmd")
if !opts.Command.G {
t.Errorf("Expected G to be true")
}
}
func TestSubcommandsOptional(t *testing.T) {
var opts = struct {
Value bool `short:"v"`
Cmd1 struct {
} `command:"remove"`
Cmd2 struct {
} `command:"add"`
}{}
p := NewParser(&opts, None)
p.SubcommandsOptional = true
_, err := p.ParseArgs([]string{"-v"})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
return
}
if !opts.Value {
t.Errorf("Expected Value to be true")
}
}
func TestCommandAlias(t *testing.T) {
var opts = struct {
Command struct {
G bool `short:"g" default:"true"`
} `command:"cmd" alias:"cm"`
}{}
assertParseSuccess(t, &opts, "cm")
if !opts.Command.G {
t.Errorf("Expected G to be true")
}
}
-304
View File
@@ -1,304 +0,0 @@
package flags
import (
"fmt"
"path/filepath"
"reflect"
"sort"
"strings"
"unicode/utf8"
)
// Completion is a type containing information of a completion.
type Completion struct {
// The completed item
Item string
// A description of the completed item (optional)
Description string
}
type completions []Completion
func (c completions) Len() int {
return len(c)
}
func (c completions) Less(i, j int) bool {
return c[i].Item < c[j].Item
}
func (c completions) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
// Completer is an interface which can be implemented by types
// to provide custom command line argument completion.
type Completer interface {
// Complete receives a prefix representing a (partial) value
// for its type and should provide a list of possible valid
// completions.
Complete(match string) []Completion
}
type completion struct {
parser *Parser
ShowDescriptions bool
}
// Filename is a string alias which provides filename completion.
type Filename string
func completionsWithoutDescriptions(items []string) []Completion {
ret := make([]Completion, len(items))
for i, v := range items {
ret[i].Item = v
}
return ret
}
// Complete returns a list of existing files with the given
// prefix.
func (f *Filename) Complete(match string) []Completion {
ret, _ := filepath.Glob(match + "*")
return completionsWithoutDescriptions(ret)
}
func (c *completion) skipPositional(s *parseState, n int) {
if n >= len(s.positional) {
s.positional = nil
} else {
s.positional = s.positional[n:]
}
}
func (c *completion) completeOptionNames(names map[string]*Option, prefix string, match string) []Completion {
n := make([]Completion, 0, len(names))
for k, opt := range names {
if strings.HasPrefix(k, match) {
n = append(n, Completion{
Item: prefix + k,
Description: opt.Description,
})
}
}
return n
}
func (c *completion) completeLongNames(s *parseState, prefix string, match string) []Completion {
return c.completeOptionNames(s.lookup.longNames, prefix, match)
}
func (c *completion) completeShortNames(s *parseState, prefix string, match string) []Completion {
if len(match) != 0 {
return []Completion{
Completion{
Item: prefix + match,
},
}
}
return c.completeOptionNames(s.lookup.shortNames, prefix, match)
}
func (c *completion) completeCommands(s *parseState, match string) []Completion {
n := make([]Completion, 0, len(s.command.commands))
for _, cmd := range s.command.commands {
if cmd.data != c && strings.HasPrefix(cmd.Name, match) {
n = append(n, Completion{
Item: cmd.Name,
Description: cmd.ShortDescription,
})
}
}
return n
}
func (c *completion) completeValue(value reflect.Value, prefix string, match string) []Completion {
i := value.Interface()
var ret []Completion
if cmp, ok := i.(Completer); ok {
ret = cmp.Complete(match)
} else if value.CanAddr() {
if cmp, ok = value.Addr().Interface().(Completer); ok {
ret = cmp.Complete(match)
}
}
for i, v := range ret {
ret[i].Item = prefix + v.Item
}
return ret
}
func (c *completion) completeArg(arg *Arg, prefix string, match string) []Completion {
if arg.isRemaining() {
// For remaining positional args (that are parsed into a slice), complete
// based on the element type.
return c.completeValue(reflect.New(arg.value.Type().Elem()), prefix, match)
}
return c.completeValue(arg.value, prefix, match)
}
func (c *completion) complete(args []string) []Completion {
if len(args) == 0 {
args = []string{""}
}
s := &parseState{
args: args,
}
c.parser.fillParseState(s)
var opt *Option
for len(s.args) > 1 {
arg := s.pop()
if (c.parser.Options&PassDoubleDash) != None && arg == "--" {
opt = nil
c.skipPositional(s, len(s.args)-1)
break
}
if argumentIsOption(arg) {
prefix, optname, islong := stripOptionPrefix(arg)
optname, _, argument := splitOption(prefix, optname, islong)
if argument == nil {
var o *Option
canarg := true
if islong {
o = s.lookup.longNames[optname]
} else {
for i, r := range optname {
sname := string(r)
o = s.lookup.shortNames[sname]
if o == nil {
break
}
if i == 0 && o.canArgument() && len(optname) != len(sname) {
canarg = false
break
}
}
}
if o == nil && (c.parser.Options&PassAfterNonOption) != None {
opt = nil
c.skipPositional(s, len(s.args)-1)
break
} else if o != nil && o.canArgument() && !o.OptionalArgument && canarg {
if len(s.args) > 1 {
s.pop()
} else {
opt = o
}
}
}
} else {
if len(s.positional) > 0 {
if !s.positional[0].isRemaining() {
// Don't advance beyond a remaining positional arg (because
// it consumes all subsequent args).
s.positional = s.positional[1:]
}
} else if cmd, ok := s.lookup.commands[arg]; ok {
cmd.fillParseState(s)
}
opt = nil
}
}
lastarg := s.args[len(s.args)-1]
var ret []Completion
if opt != nil {
// Completion for the argument of 'opt'
ret = c.completeValue(opt.value, "", lastarg)
} else if argumentStartsOption(lastarg) {
// Complete the option
prefix, optname, islong := stripOptionPrefix(lastarg)
optname, split, argument := splitOption(prefix, optname, islong)
if argument == nil && !islong {
rname, n := utf8.DecodeRuneInString(optname)
sname := string(rname)
if opt := s.lookup.shortNames[sname]; opt != nil && opt.canArgument() {
ret = c.completeValue(opt.value, prefix+sname, optname[n:])
} else {
ret = c.completeShortNames(s, prefix, optname)
}
} else if argument != nil {
if islong {
opt = s.lookup.longNames[optname]
} else {
opt = s.lookup.shortNames[optname]
}
if opt != nil {
ret = c.completeValue(opt.value, prefix+optname+split, *argument)
}
} else if islong {
ret = c.completeLongNames(s, prefix, optname)
} else {
ret = c.completeShortNames(s, prefix, optname)
}
} else if len(s.positional) > 0 {
// Complete for positional argument
ret = c.completeArg(s.positional[0], "", lastarg)
} else if len(s.command.commands) > 0 {
// Complete for command
ret = c.completeCommands(s, lastarg)
}
sort.Sort(completions(ret))
return ret
}
func (c *completion) execute(args []string) {
ret := c.complete(args)
if c.ShowDescriptions && len(ret) > 1 {
maxl := 0
for _, v := range ret {
if len(v.Item) > maxl {
maxl = len(v.Item)
}
}
for _, v := range ret {
fmt.Printf("%s", v.Item)
if len(v.Description) > 0 {
fmt.Printf("%s # %s", strings.Repeat(" ", maxl-len(v.Item)), v.Description)
}
fmt.Printf("\n")
}
} else {
for _, v := range ret {
fmt.Println(v.Item)
}
}
}
-289
View File
@@ -1,289 +0,0 @@
package flags
import (
"bytes"
"io"
"os"
"path"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
)
type TestComplete struct {
}
func (t *TestComplete) Complete(match string) []Completion {
options := []string{
"hello world",
"hello universe",
"hello multiverse",
}
ret := make([]Completion, 0, len(options))
for _, o := range options {
if strings.HasPrefix(o, match) {
ret = append(ret, Completion{
Item: o,
})
}
}
return ret
}
var completionTestOptions struct {
Verbose bool `short:"v" long:"verbose" description:"Verbose messages"`
Debug bool `short:"d" long:"debug" description:"Enable debug"`
Version bool `long:"version" description:"Show version"`
Required bool `long:"required" required:"true" description:"This is required"`
AddCommand struct {
Positional struct {
Filename Filename
} `positional-args:"yes"`
} `command:"add" description:"add an item"`
AddMultiCommand struct {
Positional struct {
Filename []Filename
} `positional-args:"yes"`
} `command:"add-multi" description:"add multiple items"`
RemoveCommand struct {
Other bool `short:"o"`
File Filename `short:"f" long:"filename"`
} `command:"rm" description:"remove an item"`
RenameCommand struct {
Completed TestComplete `short:"c" long:"completed"`
} `command:"rename" description:"rename an item"`
}
type completionTest struct {
Args []string
Completed []string
ShowDescriptions bool
}
var completionTests []completionTest
func init() {
_, sourcefile, _, _ := runtime.Caller(0)
completionTestSourcedir := filepath.Join(filepath.SplitList(path.Dir(sourcefile))...)
completionTestFilename := []string{filepath.Join(completionTestSourcedir, "completion.go"), filepath.Join(completionTestSourcedir, "completion_test.go")}
completionTests = []completionTest{
{
// Short names
[]string{"-"},
[]string{"-d", "-v"},
false,
},
{
// Short names concatenated
[]string{"-dv"},
[]string{"-dv"},
false,
},
{
// Long names
[]string{"--"},
[]string{"--debug", "--required", "--verbose", "--version"},
false,
},
{
// Long names with descriptions
[]string{"--"},
[]string{
"--debug # Enable debug",
"--required # This is required",
"--verbose # Verbose messages",
"--version # Show version",
},
true,
},
{
// Long names partial
[]string{"--ver"},
[]string{"--verbose", "--version"},
false,
},
{
// Commands
[]string{""},
[]string{"add", "add-multi", "rename", "rm"},
false,
},
{
// Commands with descriptions
[]string{""},
[]string{
"add # add an item",
"add-multi # add multiple items",
"rename # rename an item",
"rm # remove an item",
},
true,
},
{
// Commands partial
[]string{"r"},
[]string{"rename", "rm"},
false,
},
{
// Positional filename
[]string{"add", filepath.Join(completionTestSourcedir, "completion")},
completionTestFilename,
false,
},
{
// Multiple positional filename (1 arg)
[]string{"add-multi", filepath.Join(completionTestSourcedir, "completion")},
completionTestFilename,
false,
},
{
// Multiple positional filename (2 args)
[]string{"add-multi", filepath.Join(completionTestSourcedir, "completion.go"), filepath.Join(completionTestSourcedir, "completion")},
completionTestFilename,
false,
},
{
// Multiple positional filename (3 args)
[]string{"add-multi", filepath.Join(completionTestSourcedir, "completion.go"), filepath.Join(completionTestSourcedir, "completion.go"), filepath.Join(completionTestSourcedir, "completion")},
completionTestFilename,
false,
},
{
// Flag filename
[]string{"rm", "-f", path.Join(completionTestSourcedir, "completion")},
completionTestFilename,
false,
},
{
// Flag short concat last filename
[]string{"rm", "-of", path.Join(completionTestSourcedir, "completion")},
completionTestFilename,
false,
},
{
// Flag concat filename
[]string{"rm", "-f" + path.Join(completionTestSourcedir, "completion")},
[]string{"-f" + completionTestFilename[0], "-f" + completionTestFilename[1]},
false,
},
{
// Flag equal concat filename
[]string{"rm", "-f=" + path.Join(completionTestSourcedir, "completion")},
[]string{"-f=" + completionTestFilename[0], "-f=" + completionTestFilename[1]},
false,
},
{
// Flag concat long filename
[]string{"rm", "--filename=" + path.Join(completionTestSourcedir, "completion")},
[]string{"--filename=" + completionTestFilename[0], "--filename=" + completionTestFilename[1]},
false,
},
{
// Flag long filename
[]string{"rm", "--filename", path.Join(completionTestSourcedir, "completion")},
completionTestFilename,
false,
},
{
// Custom completed
[]string{"rename", "-c", "hello un"},
[]string{"hello universe"},
false,
},
}
}
func TestCompletion(t *testing.T) {
p := NewParser(&completionTestOptions, Default)
c := &completion{parser: p}
for _, test := range completionTests {
if test.ShowDescriptions {
continue
}
ret := c.complete(test.Args)
items := make([]string, len(ret))
for i, v := range ret {
items[i] = v.Item
}
if !reflect.DeepEqual(items, test.Completed) {
t.Errorf("Args: %#v, %#v\n Expected: %#v\n Got: %#v", test.Args, test.ShowDescriptions, test.Completed, items)
}
}
}
func TestParserCompletion(t *testing.T) {
for _, test := range completionTests {
if test.ShowDescriptions {
os.Setenv("GO_FLAGS_COMPLETION", "verbose")
} else {
os.Setenv("GO_FLAGS_COMPLETION", "1")
}
tmp := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
out := make(chan string)
go func() {
var buf bytes.Buffer
io.Copy(&buf, r)
out <- buf.String()
}()
p := NewParser(&completionTestOptions, None)
_, err := p.ParseArgs(test.Args)
w.Close()
os.Stdout = tmp
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
got := strings.Split(strings.Trim(<-out, "\n"), "\n")
if !reflect.DeepEqual(got, test.Completed) {
t.Errorf("Expected: %#v\nGot: %#v", test.Completed, got)
}
}
os.Setenv("GO_FLAGS_COMPLETION", "")
}
-357
View File
@@ -1,357 +0,0 @@
// Copyright 2012 Jesse van den Kieboom. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package flags
import (
"fmt"
"reflect"
"strconv"
"strings"
"time"
)
// Marshaler is the interface implemented by types that can marshal themselves
// to a string representation of the flag.
type Marshaler interface {
// MarshalFlag marshals a flag value to its string representation.
MarshalFlag() (string, error)
}
// Unmarshaler is the interface implemented by types that can unmarshal a flag
// argument to themselves. The provided value is directly passed from the
// command line.
type Unmarshaler interface {
// UnmarshalFlag unmarshals a string value representation to the flag
// value (which therefore needs to be a pointer receiver).
UnmarshalFlag(value string) error
}
func getBase(options multiTag, base int) (int, error) {
sbase := options.Get("base")
var err error
var ivbase int64
if sbase != "" {
ivbase, err = strconv.ParseInt(sbase, 10, 32)
base = int(ivbase)
}
return base, err
}
func convertMarshal(val reflect.Value) (bool, string, error) {
// Check first for the Marshaler interface
if val.Type().NumMethod() > 0 && val.CanInterface() {
if marshaler, ok := val.Interface().(Marshaler); ok {
ret, err := marshaler.MarshalFlag()
return true, ret, err
}
}
return false, "", nil
}
func convertToString(val reflect.Value, options multiTag) (string, error) {
if ok, ret, err := convertMarshal(val); ok {
return ret, err
}
tp := val.Type()
// Support for time.Duration
if tp == reflect.TypeOf((*time.Duration)(nil)).Elem() {
stringer := val.Interface().(fmt.Stringer)
return stringer.String(), nil
}
switch tp.Kind() {
case reflect.String:
return val.String(), nil
case reflect.Bool:
if val.Bool() {
return "true", nil
}
return "false", nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
base, err := getBase(options, 10)
if err != nil {
return "", err
}
return strconv.FormatInt(val.Int(), base), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
base, err := getBase(options, 10)
if err != nil {
return "", err
}
return strconv.FormatUint(val.Uint(), base), nil
case reflect.Float32, reflect.Float64:
return strconv.FormatFloat(val.Float(), 'g', -1, tp.Bits()), nil
case reflect.Slice:
if val.Len() == 0 {
return "", nil
}
ret := "["
for i := 0; i < val.Len(); i++ {
if i != 0 {
ret += ", "
}
item, err := convertToString(val.Index(i), options)
if err != nil {
return "", err
}
ret += item
}
return ret + "]", nil
case reflect.Map:
ret := "{"
for i, key := range val.MapKeys() {
if i != 0 {
ret += ", "
}
keyitem, err := convertToString(key, options)
if err != nil {
return "", err
}
item, err := convertToString(val.MapIndex(key), options)
if err != nil {
return "", err
}
ret += keyitem + ":" + item
}
return ret + "}", nil
case reflect.Ptr:
return convertToString(reflect.Indirect(val), options)
case reflect.Interface:
if !val.IsNil() {
return convertToString(val.Elem(), options)
}
}
return "", nil
}
func convertUnmarshal(val string, retval reflect.Value) (bool, error) {
if retval.Type().NumMethod() > 0 && retval.CanInterface() {
if unmarshaler, ok := retval.Interface().(Unmarshaler); ok {
return true, unmarshaler.UnmarshalFlag(val)
}
}
if retval.Type().Kind() != reflect.Ptr && retval.CanAddr() {
return convertUnmarshal(val, retval.Addr())
}
if retval.Type().Kind() == reflect.Interface && !retval.IsNil() {
return convertUnmarshal(val, retval.Elem())
}
return false, nil
}
func convert(val string, retval reflect.Value, options multiTag) error {
if ok, err := convertUnmarshal(val, retval); ok {
return err
}
tp := retval.Type()
// Support for time.Duration
if tp == reflect.TypeOf((*time.Duration)(nil)).Elem() {
parsed, err := time.ParseDuration(val)
if err != nil {
return err
}
retval.SetInt(int64(parsed))
return nil
}
switch tp.Kind() {
case reflect.String:
retval.SetString(val)
case reflect.Bool:
if val == "" {
retval.SetBool(true)
} else {
b, err := strconv.ParseBool(val)
if err != nil {
return err
}
retval.SetBool(b)
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
base, err := getBase(options, 10)
if err != nil {
return err
}
parsed, err := strconv.ParseInt(val, base, tp.Bits())
if err != nil {
return err
}
retval.SetInt(parsed)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
base, err := getBase(options, 10)
if err != nil {
return err
}
parsed, err := strconv.ParseUint(val, base, tp.Bits())
if err != nil {
return err
}
retval.SetUint(parsed)
case reflect.Float32, reflect.Float64:
parsed, err := strconv.ParseFloat(val, tp.Bits())
if err != nil {
return err
}
retval.SetFloat(parsed)
case reflect.Slice:
elemtp := tp.Elem()
elemvalptr := reflect.New(elemtp)
elemval := reflect.Indirect(elemvalptr)
if err := convert(val, elemval, options); err != nil {
return err
}
retval.Set(reflect.Append(retval, elemval))
case reflect.Map:
parts := strings.SplitN(val, ":", 2)
key := parts[0]
var value string
if len(parts) == 2 {
value = parts[1]
}
keytp := tp.Key()
keyval := reflect.New(keytp)
if err := convert(key, keyval, options); err != nil {
return err
}
valuetp := tp.Elem()
valueval := reflect.New(valuetp)
if err := convert(value, valueval, options); err != nil {
return err
}
if retval.IsNil() {
retval.Set(reflect.MakeMap(tp))
}
retval.SetMapIndex(reflect.Indirect(keyval), reflect.Indirect(valueval))
case reflect.Ptr:
if retval.IsNil() {
retval.Set(reflect.New(retval.Type().Elem()))
}
return convert(val, reflect.Indirect(retval), options)
case reflect.Interface:
if !retval.IsNil() {
return convert(val, retval.Elem(), options)
}
}
return nil
}
func isPrint(s string) bool {
for _, c := range s {
if !strconv.IsPrint(c) {
return false
}
}
return true
}
func quoteIfNeeded(s string) string {
if !isPrint(s) {
return strconv.Quote(s)
}
return s
}
func unquoteIfPossible(s string) (string, error) {
if len(s) == 0 || s[0] != '"' {
return s, nil
}
return strconv.Unquote(s)
}
func wrapText(s string, l int, prefix string) string {
// Basic text wrapping of s at spaces to fit in l
var ret string
s = strings.TrimSpace(s)
for len(s) > l {
// Try to split on space
suffix := ""
pos := strings.LastIndex(s[:l], " ")
if pos < 0 {
pos = l - 1
suffix = "-\n"
}
if len(ret) != 0 {
ret += "\n" + prefix
}
ret += strings.TrimSpace(s[:pos]) + suffix
s = strings.TrimSpace(s[pos:])
}
if len(s) > 0 {
if len(ret) != 0 {
ret += "\n" + prefix
}
return ret + s
}
return ret
}
-175
View File
@@ -1,175 +0,0 @@
package flags
import (
"testing"
"time"
)
func expectConvert(t *testing.T, o *Option, expected string) {
s, err := convertToString(o.value, o.tag)
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
assertString(t, s, expected)
}
func TestConvertToString(t *testing.T) {
d, _ := time.ParseDuration("1h2m4s")
var opts = struct {
String string `long:"string"`
Int int `long:"int"`
Int8 int8 `long:"int8"`
Int16 int16 `long:"int16"`
Int32 int32 `long:"int32"`
Int64 int64 `long:"int64"`
Uint uint `long:"uint"`
Uint8 uint8 `long:"uint8"`
Uint16 uint16 `long:"uint16"`
Uint32 uint32 `long:"uint32"`
Uint64 uint64 `long:"uint64"`
Float32 float32 `long:"float32"`
Float64 float64 `long:"float64"`
Duration time.Duration `long:"duration"`
Bool bool `long:"bool"`
IntSlice []int `long:"int-slice"`
IntFloatMap map[int]float64 `long:"int-float-map"`
PtrBool *bool `long:"ptr-bool"`
Interface interface{} `long:"interface"`
Int32Base int32 `long:"int32-base" base:"16"`
Uint32Base uint32 `long:"uint32-base" base:"16"`
}{
"string",
-2,
-1,
0,
1,
2,
1,
2,
3,
4,
5,
1.2,
-3.4,
d,
true,
[]int{-3, 4, -2},
map[int]float64{-2: 4.5},
new(bool),
float32(5.2),
-5823,
4232,
}
p := NewNamedParser("test", Default)
grp, _ := p.AddGroup("test group", "", &opts)
expects := []string{
"string",
"-2",
"-1",
"0",
"1",
"2",
"1",
"2",
"3",
"4",
"5",
"1.2",
"-3.4",
"1h2m4s",
"true",
"[-3, 4, -2]",
"{-2:4.5}",
"false",
"5.2",
"-16bf",
"1088",
}
for i, v := range grp.Options() {
expectConvert(t, v, expects[i])
}
}
func TestConvertToStringInvalidIntBase(t *testing.T) {
var opts = struct {
Int int `long:"int" base:"no"`
}{
2,
}
p := NewNamedParser("test", Default)
grp, _ := p.AddGroup("test group", "", &opts)
o := grp.Options()[0]
_, err := convertToString(o.value, o.tag)
if err != nil {
err = newErrorf(ErrMarshal, "%v", err)
}
assertError(t, err, ErrMarshal, "strconv.ParseInt: parsing \"no\": invalid syntax")
}
func TestConvertToStringInvalidUintBase(t *testing.T) {
var opts = struct {
Uint uint `long:"uint" base:"no"`
}{
2,
}
p := NewNamedParser("test", Default)
grp, _ := p.AddGroup("test group", "", &opts)
o := grp.Options()[0]
_, err := convertToString(o.value, o.tag)
if err != nil {
err = newErrorf(ErrMarshal, "%v", err)
}
assertError(t, err, ErrMarshal, "strconv.ParseInt: parsing \"no\": invalid syntax")
}
func TestWrapText(t *testing.T) {
s := "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
got := wrapText(s, 60, " ")
expected := `Lorem ipsum dolor sit amet, consectetur adipisicing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit
esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
occaecat cupidatat non proident, sunt in culpa qui officia
deserunt mollit anim id est laborum.`
assertDiff(t, got, expected, "wrapped text")
}
-123
View File
@@ -1,123 +0,0 @@
package flags
import (
"fmt"
)
// ErrorType represents the type of error.
type ErrorType uint
const (
// ErrUnknown indicates a generic error.
ErrUnknown ErrorType = iota
// ErrExpectedArgument indicates that an argument was expected.
ErrExpectedArgument
// ErrUnknownFlag indicates an unknown flag.
ErrUnknownFlag
// ErrUnknownGroup indicates an unknown group.
ErrUnknownGroup
// ErrMarshal indicates a marshalling error while converting values.
ErrMarshal
// ErrHelp indicates that the built-in help was shown (the error
// contains the help message).
ErrHelp
// ErrNoArgumentForBool indicates that an argument was given for a
// boolean flag (which don't not take any arguments).
ErrNoArgumentForBool
// ErrRequired indicates that a required flag was not provided.
ErrRequired
// ErrShortNameTooLong indicates that a short flag name was specified,
// longer than one character.
ErrShortNameTooLong
// ErrDuplicatedFlag indicates that a short or long flag has been
// defined more than once
ErrDuplicatedFlag
// ErrTag indicates an error while parsing flag tags.
ErrTag
// ErrCommandRequired indicates that a command was required but not
// specified
ErrCommandRequired
// ErrUnknownCommand indicates that an unknown command was specified.
ErrUnknownCommand
)
func (e ErrorType) String() string {
switch e {
case ErrUnknown:
return "unknown"
case ErrExpectedArgument:
return "expected argument"
case ErrUnknownFlag:
return "unknown flag"
case ErrUnknownGroup:
return "unknown group"
case ErrMarshal:
return "marshal"
case ErrHelp:
return "help"
case ErrNoArgumentForBool:
return "no argument for bool"
case ErrRequired:
return "required"
case ErrShortNameTooLong:
return "short name too long"
case ErrDuplicatedFlag:
return "duplicated flag"
case ErrTag:
return "tag"
case ErrCommandRequired:
return "command required"
case ErrUnknownCommand:
return "unknown command"
}
return "unrecognized error type"
}
// Error represents a parser error. The error returned from Parse is of this
// type. The error contains both a Type and Message.
type Error struct {
// The type of error
Type ErrorType
// The error message
Message string
}
// Error returns the error's message
func (e *Error) Error() string {
return e.Message
}
func newError(tp ErrorType, message string) *Error {
return &Error{
Type: tp,
Message: message,
}
}
func newErrorf(tp ErrorType, format string, args ...interface{}) *Error {
return newError(tp, fmt.Sprintf(format, args...))
}
func wrapError(err error) *Error {
ret, ok := err.(*Error)
if !ok {
return newError(ErrUnknown, err.Error())
}
return ret
}
-110
View File
@@ -1,110 +0,0 @@
// Example of use of the flags package.
package flags
import (
"fmt"
"os/exec"
)
func Example() {
var opts struct {
// Slice of bool will append 'true' each time the option
// is encountered (can be set multiple times, like -vvv)
Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"`
// Example of automatic marshalling to desired type (uint)
Offset uint `long:"offset" description:"Offset"`
// Example of a callback, called each time the option is found.
Call func(string) `short:"c" description:"Call phone number"`
// Example of a required flag
Name string `short:"n" long:"name" description:"A name" required:"true"`
// Example of a value name
File string `short:"f" long:"file" description:"A file" value-name:"FILE"`
// Example of a pointer
Ptr *int `short:"p" description:"A pointer to an integer"`
// Example of a slice of strings
StringSlice []string `short:"s" description:"A slice of strings"`
// Example of a slice of pointers
PtrSlice []*string `long:"ptrslice" description:"A slice of pointers to string"`
// Example of a map
IntMap map[string]int `long:"intmap" description:"A map from string to int"`
// Example of a filename (useful for completion)
Filename Filename `long:"filename" description:"A filename"`
// Example of positional arguments
Args struct {
Id string
Num int
Rest []string
} `positional-args:"yes" required:"yes"`
}
// Callback which will invoke callto:<argument> to call a number.
// Note that this works just on OS X (and probably only with
// Skype) but it shows the idea.
opts.Call = func(num string) {
cmd := exec.Command("open", "callto:"+num)
cmd.Start()
cmd.Process.Release()
}
// Make some fake arguments to parse.
args := []string{
"-vv",
"--offset=5",
"-n", "Me",
"-p", "3",
"-s", "hello",
"-s", "world",
"--ptrslice", "hello",
"--ptrslice", "world",
"--intmap", "a:1",
"--intmap", "b:5",
"--filename", "hello.go",
"id",
"10",
"remaining1",
"remaining2",
}
// Parse flags from `args'. Note that here we use flags.ParseArgs for
// the sake of making a working example. Normally, you would simply use
// flags.Parse(&opts) which uses os.Args
_, err := ParseArgs(&opts, args)
if err != nil {
panic(err)
}
fmt.Printf("Verbosity: %v\n", opts.Verbose)
fmt.Printf("Offset: %d\n", opts.Offset)
fmt.Printf("Name: %s\n", opts.Name)
fmt.Printf("Ptr: %d\n", *opts.Ptr)
fmt.Printf("StringSlice: %v\n", opts.StringSlice)
fmt.Printf("PtrSlice: [%v %v]\n", *opts.PtrSlice[0], *opts.PtrSlice[1])
fmt.Printf("IntMap: [a:%v b:%v]\n", opts.IntMap["a"], opts.IntMap["b"])
fmt.Printf("Filename: %v\n", opts.Filename)
fmt.Printf("Args.Id: %s\n", opts.Args.Id)
fmt.Printf("Args.Num: %d\n", opts.Args.Num)
fmt.Printf("Args.Rest: %v\n", opts.Args.Rest)
// Output: Verbosity: [true true]
// Offset: 5
// Name: Me
// Ptr: 3
// StringSlice: [hello world]
// PtrSlice: [hello world]
// IntMap: [a:1 b:5]
// Filename: hello.go
// Args.Id: id
// Args.Num: 10
// Args.Rest: [remaining1 remaining2]
}
-23
View File
@@ -1,23 +0,0 @@
package main
import (
"fmt"
)
type AddCommand struct {
All bool `short:"a" long:"all" description:"Add all files"`
}
var addCommand AddCommand
func (x *AddCommand) Execute(args []string) error {
fmt.Printf("Adding (all=%v): %#v\n", x.All, args)
return nil
}
func init() {
parser.AddCommand("add",
"Add a file",
"The add command adds a file to the repository. Use -a to add all files.",
&addCommand)
}
@@ -1,9 +0,0 @@
_examples() {
args=("${COMP_WORDS[@]:1:$COMP_CWORD}")
local IFS=$'\n'
COMPREPLY=($(GO_FLAGS_COMPLETION=1 ${COMP_WORDS[0]} "${args[@]}"))
return 1
}
complete -F _examples examples
-75
View File
@@ -1,75 +0,0 @@
package main
import (
"errors"
"fmt"
"github.com/jessevdk/go-flags"
"os"
"strconv"
"strings"
)
type EditorOptions struct {
Input flags.Filename `short:"i" long:"input" description:"Input file" default:"-"`
Output flags.Filename `short:"o" long:"output" description:"Output file" default:"-"`
}
type Point struct {
X, Y int
}
func (p *Point) UnmarshalFlag(value string) error {
parts := strings.Split(value, ",")
if len(parts) != 2 {
return errors.New("expected two numbers separated by a ,")
}
x, err := strconv.ParseInt(parts[0], 10, 32)
if err != nil {
return err
}
y, err := strconv.ParseInt(parts[1], 10, 32)
if err != nil {
return err
}
p.X = int(x)
p.Y = int(y)
return nil
}
func (p Point) MarshalFlag() (string, error) {
return fmt.Sprintf("%d,%d", p.X, p.Y), nil
}
type Options struct {
// Example of verbosity with level
Verbose []bool `short:"v" long:"verbose" description:"Verbose output"`
// Example of optional value
User string `short:"u" long:"user" description:"User name" optional:"yes" optional-value:"pancake"`
// Example of map with multiple default values
Users map[string]string `long:"users" description:"User e-mail map" default:"system:system@example.org" default:"admin:admin@example.org"`
// Example of option group
Editor EditorOptions `group:"Editor Options"`
// Example of custom type Marshal/Unmarshal
Point Point `long:"point" description:"A x,y point" default:"1,2"`
}
var options Options
var parser = flags.NewParser(&options, flags.Default)
func main() {
if _, err := parser.Parse(); err != nil {
os.Exit(1)
}
}
-23
View File
@@ -1,23 +0,0 @@
package main
import (
"fmt"
)
type RmCommand struct {
Force bool `short:"f" long:"force" description:"Force removal of files"`
}
var rmCommand RmCommand
func (x *RmCommand) Execute(args []string) error {
fmt.Printf("Removing (force=%v): %#v\n", x.Force, args)
return nil
}
func init() {
parser.AddCommand("rm",
"Remove a file",
"The rm command removes a file to the repository. Use -f to force removal of files.",
&rmCommand)
}
-238
View File
@@ -1,238 +0,0 @@
// Copyright 2012 Jesse van den Kieboom. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package flags provides an extensive command line option parser.
The flags package is similar in functionality to the go built-in flag package
but provides more options and uses reflection to provide a convenient and
succinct way of specifying command line options.
Supported features
The following features are supported in go-flags:
Options with short names (-v)
Options with long names (--verbose)
Options with and without arguments (bool v.s. other type)
Options with optional arguments and default values
Option default values from ENVIRONMENT_VARIABLES, including slice and map values
Multiple option groups each containing a set of options
Generate and print well-formatted help message
Passing remaining command line arguments after -- (optional)
Ignoring unknown command line options (optional)
Supports -I/usr/include -I=/usr/include -I /usr/include option argument specification
Supports multiple short options -aux
Supports all primitive go types (string, int{8..64}, uint{8..64}, float)
Supports same option multiple times (can store in slice or last option counts)
Supports maps
Supports function callbacks
Supports namespaces for (nested) option groups
Additional features specific to Windows:
Options with short names (/v)
Options with long names (/verbose)
Windows-style options with arguments use a colon as the delimiter
Modify generated help message with Windows-style / options
Basic usage
The flags package uses structs, reflection and struct field tags
to allow users to specify command line options. This results in very simple
and concise specification of your application options. For example:
type Options struct {
Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"`
}
This specifies one option with a short name -v and a long name --verbose.
When either -v or --verbose is found on the command line, a 'true' value
will be appended to the Verbose field. e.g. when specifying -vvv, the
resulting value of Verbose will be {[true, true, true]}.
Slice options work exactly the same as primitive type options, except that
whenever the option is encountered, a value is appended to the slice.
Map options from string to primitive type are also supported. On the command
line, you specify the value for such an option as key:value. For example
type Options struct {
AuthorInfo string[string] `short:"a"`
}
Then, the AuthorInfo map can be filled with something like
-a name:Jesse -a "surname:van den Kieboom".
Finally, for full control over the conversion between command line argument
values and options, user defined types can choose to implement the Marshaler
and Unmarshaler interfaces.
Available field tags
The following is a list of tags for struct fields supported by go-flags:
short: the short name of the option (single character)
long: the long name of the option
required: whether an option is required to appear on the command
line. If a required option is not present, the parser will
return ErrRequired (optional)
description: the description of the option (optional)
long-description: the long description of the option. Currently only
displayed in generated man pages (optional)
no-flag: if non-empty this field is ignored as an option (optional)
optional: whether an argument of the option is optional (optional)
optional-value: the value of an optional option when the option occurs
without an argument. This tag can be specified multiple
times in the case of maps or slices (optional)
default: the default value of an option. This tag can be specified
multiple times in the case of slices or maps (optional)
default-mask: when specified, this value will be displayed in the help
instead of the actual default value. This is useful
mostly for hiding otherwise sensitive information from
showing up in the help. If default-mask takes the special
value "-", then no default value will be shown at all
(optional)
env: the default value of the option is overridden from the
specified environment variable, if one has been defined.
(optional)
env-delim: the 'env' default value from environment is split into
multiple values with the given delimiter string, use with
slices and maps (optional)
value-name: the name of the argument value (to be shown in the help,
(optional)
base: a base (radix) used to convert strings to integer values, the
default base is 10 (i.e. decimal) (optional)
ini-name: the explicit ini option name (optional)
no-ini: if non-empty this field is ignored as an ini option
(optional)
group: when specified on a struct field, makes the struct
field a separate group with the given name (optional)
namespace: when specified on a group struct field, the namespace
gets prepended to every option's long name and
subgroup's namespace of this group, separated by
the parser's namespace delimiter (optional)
command: when specified on a struct field, makes the struct
field a (sub)command with the given name (optional)
subcommands-optional: when specified on a command struct field, makes
any subcommands of that command optional (optional)
alias: when specified on a command struct field, adds the
specified name as an alias for the command. Can be
be specified multiple times to add more than one
alias (optional)
positional-args: when specified on a field with a struct type,
uses the fields of that struct to parse remaining
positional command line arguments into (in order
of the fields). If a field has a slice type,
then all remaining arguments will be added to it.
Positional arguments are optional by default,
unless the "required" tag is specified together
with the "positional-args" tag (optional)
Either the `short:` tag or the `long:` must be specified to make the field eligible as an
option.
Option groups
Option groups are a simple way to semantically separate your options. All
options in a particular group are shown together in the help under the name
of the group. Namespaces can be used to specify option long names more
precisely and emphasize the options affiliation to their group.
There are currently three ways to specify option groups.
1. Use NewNamedParser specifying the various option groups.
2. Use AddGroup to add a group to an existing parser.
3. Add a struct field to the top-level options annotated with the
group:"group-name" tag.
Commands
The flags package also has basic support for commands. Commands are often
used in monolithic applications that support various commands or actions.
Take git for example, all of the add, commit, checkout, etc. are called
commands. Using commands you can easily separate multiple functions of your
application.
There are currently two ways to specify a command.
1. Use AddCommand on an existing parser.
2. Add a struct field to your options struct annotated with the
command:"command-name" tag.
The most common, idiomatic way to implement commands is to define a global
parser instance and implement each command in a separate file. These
command files should define a go init function which calls AddCommand on
the global parser.
When parsing ends and there is an active command and that command implements
the Commander interface, then its Execute method will be run with the
remaining command line arguments.
Command structs can have options which become valid to parse after the
command has been specified on the command line. It is currently not valid
to specify options from the parent level of the command after the command
name has occurred. Thus, given a top-level option "-v" and a command "add":
Valid: ./app -v add
Invalid: ./app add -v
Completion
go-flags has builtin support to provide bash completion of flags, commands
and argument values. To use completion, the binary which uses go-flags
can be invoked in a special environment to list completion of the current
command line argument. It should be noted that this `executes` your application,
and it is up to the user to make sure there are no negative side effects (for
example from init functions).
Setting the environment variable `GO_FLAGS_COMPLETION=1` enables completion
by replacing the argument parsing routine with the completion routine which
outputs completions for the passed arguments. The basic invocation to
complete a set of arguments is therefore:
GO_FLAGS_COMPLETION=1 ./completion-example arg1 arg2 arg3
where `completion-example` is the binary, `arg1` and `arg2` are
the current arguments, and `arg3` (the last argument) is the argument
to be completed. If the GO_FLAGS_COMPLETION is set to "verbose", then
descriptions of possible completion items will also be shown, if there
are more than 1 completion items.
To use this with bash completion, a simple file can be written which
calls the binary which supports go-flags completion:
_completion_example() {
# All arguments except the first one
args=("${COMP_WORDS[@]:1:$COMP_CWORD}")
# Only split on newlines
local IFS=$'\n'
# Call completion (note that the first element of COMP_WORDS is
# the executable itself)
COMPREPLY=($(GO_FLAGS_COMPLETION=1 ${COMP_WORDS[0]} "${args[@]}"))
return 0
}
complete -F _completion_example completion-example
Completion requires the parser option PassDoubleDash and is therefore enforced if the environment variable GO_FLAGS_COMPLETION is set.
Customized completion for argument values is supported by implementing
the flags.Completer interface for the argument value type. An example
of a type which does so is the flags.Filename type, an alias of string
allowing simple filename completion. A slice or array argument value
whose element type implements flags.Completer will also be completed.
*/
package flags
-91
View File
@@ -1,91 +0,0 @@
// Copyright 2012 Jesse van den Kieboom. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package flags
import (
"errors"
"strings"
)
// ErrNotPointerToStruct indicates that a provided data container is not
// a pointer to a struct. Only pointers to structs are valid data containers
// for options.
var ErrNotPointerToStruct = errors.New("provided data is not a pointer to struct")
// Group represents an option group. Option groups can be used to logically
// group options together under a description. Groups are only used to provide
// more structure to options both for the user (as displayed in the help message)
// and for you, since groups can be nested.
type Group struct {
// A short description of the group. The
// short description is primarily used in the built-in generated help
// message
ShortDescription string
// A long description of the group. The long
// description is primarily used to present information on commands
// (Command embeds Group) in the built-in generated help and man pages.
LongDescription string
// The namespace of the group
Namespace string
// The parent of the group or nil if it has no parent
parent interface{}
// All the options in the group
options []*Option
// All the subgroups
groups []*Group
// Whether the group represents the built-in help group
isBuiltinHelp bool
data interface{}
}
// AddGroup adds a new group to the command with the given name and data. The
// data needs to be a pointer to a struct from which the fields indicate which
// options are in the group.
func (g *Group) AddGroup(shortDescription string, longDescription string, data interface{}) (*Group, error) {
group := newGroup(shortDescription, longDescription, data)
group.parent = g
if err := group.scan(); err != nil {
return nil, err
}
g.groups = append(g.groups, group)
return group, nil
}
// Groups returns the list of groups embedded in this group.
func (g *Group) Groups() []*Group {
return g.groups
}
// Options returns the list of options in this group.
func (g *Group) Options() []*Option {
return g.options
}
// Find locates the subgroup with the given short description and returns it.
// If no such group can be found Find will return nil. Note that the description
// is matched case insensitively.
func (g *Group) Find(shortDescription string) *Group {
lshortDescription := strings.ToLower(shortDescription)
var ret *Group
g.eachGroup(func(gg *Group) {
if gg != g && strings.ToLower(gg.ShortDescription) == lshortDescription {
ret = gg
}
})
return ret
}
-254
View File
@@ -1,254 +0,0 @@
package flags
import (
"reflect"
"unicode/utf8"
"unsafe"
)
type scanHandler func(reflect.Value, *reflect.StructField) (bool, error)
func newGroup(shortDescription string, longDescription string, data interface{}) *Group {
return &Group{
ShortDescription: shortDescription,
LongDescription: longDescription,
data: data,
}
}
func (g *Group) optionByName(name string, namematch func(*Option, string) bool) *Option {
prio := 0
var retopt *Option
for _, opt := range g.options {
if namematch != nil && namematch(opt, name) && prio < 4 {
retopt = opt
prio = 4
}
if name == opt.field.Name && prio < 3 {
retopt = opt
prio = 3
}
if name == opt.LongNameWithNamespace() && prio < 2 {
retopt = opt
prio = 2
}
if opt.ShortName != 0 && name == string(opt.ShortName) && prio < 1 {
retopt = opt
prio = 1
}
}
return retopt
}
func (g *Group) eachGroup(f func(*Group)) {
f(g)
for _, gg := range g.groups {
gg.eachGroup(f)
}
}
func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, handler scanHandler) error {
stype := realval.Type()
if sfield != nil {
if ok, err := handler(realval, sfield); err != nil {
return err
} else if ok {
return nil
}
}
for i := 0; i < stype.NumField(); i++ {
field := stype.Field(i)
// PkgName is set only for non-exported fields, which we ignore
if field.PkgPath != "" {
continue
}
mtag := newMultiTag(string(field.Tag))
if err := mtag.Parse(); err != nil {
return err
}
// Skip fields with the no-flag tag
if mtag.Get("no-flag") != "" {
continue
}
// Dive deep into structs or pointers to structs
kind := field.Type.Kind()
fld := realval.Field(i)
if kind == reflect.Struct {
if err := g.scanStruct(fld, &field, handler); err != nil {
return err
}
} else if kind == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct {
if fld.IsNil() {
fld.Set(reflect.New(fld.Type().Elem()))
}
if err := g.scanStruct(reflect.Indirect(fld), &field, handler); err != nil {
return err
}
}
longname := mtag.Get("long")
shortname := mtag.Get("short")
// Need at least either a short or long name
if longname == "" && shortname == "" && mtag.Get("ini-name") == "" {
continue
}
short := rune(0)
rc := utf8.RuneCountInString(shortname)
if rc > 1 {
return newErrorf(ErrShortNameTooLong,
"short names can only be 1 character long, not `%s'",
shortname)
} else if rc == 1 {
short, _ = utf8.DecodeRuneInString(shortname)
}
description := mtag.Get("description")
def := mtag.GetMany("default")
optionalValue := mtag.GetMany("optional-value")
valueName := mtag.Get("value-name")
defaultMask := mtag.Get("default-mask")
optional := (mtag.Get("optional") != "")
required := (mtag.Get("required") != "")
option := &Option{
Description: description,
ShortName: short,
LongName: longname,
Default: def,
EnvDefaultKey: mtag.Get("env"),
EnvDefaultDelim: mtag.Get("env-delim"),
OptionalArgument: optional,
OptionalValue: optionalValue,
Required: required,
ValueName: valueName,
DefaultMask: defaultMask,
group: g,
field: field,
value: realval.Field(i),
tag: mtag,
}
g.options = append(g.options, option)
}
return nil
}
func (g *Group) checkForDuplicateFlags() *Error {
shortNames := make(map[rune]*Option)
longNames := make(map[string]*Option)
var duplicateError *Error
g.eachGroup(func(g *Group) {
for _, option := range g.options {
if option.LongName != "" {
longName := option.LongNameWithNamespace()
if otherOption, ok := longNames[longName]; ok {
duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same long name as option `%s'", option, otherOption)
return
}
longNames[longName] = option
}
if option.ShortName != 0 {
if otherOption, ok := shortNames[option.ShortName]; ok {
duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same short name as option `%s'", option, otherOption)
return
}
shortNames[option.ShortName] = option
}
}
})
return duplicateError
}
func (g *Group) scanSubGroupHandler(realval reflect.Value, sfield *reflect.StructField) (bool, error) {
mtag := newMultiTag(string(sfield.Tag))
if err := mtag.Parse(); err != nil {
return true, err
}
subgroup := mtag.Get("group")
if len(subgroup) != 0 {
ptrval := reflect.NewAt(realval.Type(), unsafe.Pointer(realval.UnsafeAddr()))
description := mtag.Get("description")
group, err := g.AddGroup(subgroup, description, ptrval.Interface())
if err != nil {
return true, err
}
group.Namespace = mtag.Get("namespace")
return true, nil
}
return false, nil
}
func (g *Group) scanType(handler scanHandler) error {
// Get all the public fields in the data struct
ptrval := reflect.ValueOf(g.data)
if ptrval.Type().Kind() != reflect.Ptr {
panic(ErrNotPointerToStruct)
}
stype := ptrval.Type().Elem()
if stype.Kind() != reflect.Struct {
panic(ErrNotPointerToStruct)
}
realval := reflect.Indirect(ptrval)
if err := g.scanStruct(realval, nil, handler); err != nil {
return err
}
if err := g.checkForDuplicateFlags(); err != nil {
return err
}
return nil
}
func (g *Group) scan() error {
return g.scanType(g.scanSubGroupHandler)
}
func (g *Group) groupByName(name string) *Group {
if len(name) == 0 {
return g
}
return g.Find(name)
}
-187
View File
@@ -1,187 +0,0 @@
package flags
import (
"testing"
)
func TestGroupInline(t *testing.T) {
var opts = struct {
Value bool `short:"v"`
Group struct {
G bool `short:"g"`
} `group:"Grouped Options"`
}{}
p, ret := assertParserSuccess(t, &opts, "-v", "-g")
assertStringArray(t, ret, []string{})
if !opts.Value {
t.Errorf("Expected Value to be true")
}
if !opts.Group.G {
t.Errorf("Expected Group.G to be true")
}
if p.Command.Group.Find("Grouped Options") == nil {
t.Errorf("Expected to find group `Grouped Options'")
}
}
func TestGroupAdd(t *testing.T) {
var opts = struct {
Value bool `short:"v"`
}{}
var grp = struct {
G bool `short:"g"`
}{}
p := NewParser(&opts, Default)
g, err := p.AddGroup("Grouped Options", "", &grp)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
return
}
ret, err := p.ParseArgs([]string{"-v", "-g", "rest"})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
return
}
assertStringArray(t, ret, []string{"rest"})
if !opts.Value {
t.Errorf("Expected Value to be true")
}
if !grp.G {
t.Errorf("Expected Group.G to be true")
}
if p.Command.Group.Find("Grouped Options") != g {
t.Errorf("Expected to find group `Grouped Options'")
}
if p.Groups()[1] != g {
t.Errorf("Expected group %#v, but got %#v", g, p.Groups()[0])
}
if g.Options()[0].ShortName != 'g' {
t.Errorf("Expected short name `g' but got %v", g.Options()[0].ShortName)
}
}
func TestGroupNestedInline(t *testing.T) {
var opts = struct {
Value bool `short:"v"`
Group struct {
G bool `short:"g"`
Nested struct {
N string `long:"n"`
} `group:"Nested Options"`
} `group:"Grouped Options"`
}{}
p, ret := assertParserSuccess(t, &opts, "-v", "-g", "--n", "n", "rest")
assertStringArray(t, ret, []string{"rest"})
if !opts.Value {
t.Errorf("Expected Value to be true")
}
if !opts.Group.G {
t.Errorf("Expected Group.G to be true")
}
assertString(t, opts.Group.Nested.N, "n")
if p.Command.Group.Find("Grouped Options") == nil {
t.Errorf("Expected to find group `Grouped Options'")
}
if p.Command.Group.Find("Nested Options") == nil {
t.Errorf("Expected to find group `Nested Options'")
}
}
func TestGroupNestedInlineNamespace(t *testing.T) {
var opts = struct {
Opt string `long:"opt"`
Group struct {
Opt string `long:"opt"`
Group struct {
Opt string `long:"opt"`
} `group:"Subsubgroup" namespace:"sap"`
} `group:"Subgroup" namespace:"sip"`
}{}
p, ret := assertParserSuccess(t, &opts, "--opt", "a", "--sip.opt", "b", "--sip.sap.opt", "c", "rest")
assertStringArray(t, ret, []string{"rest"})
assertString(t, opts.Opt, "a")
assertString(t, opts.Group.Opt, "b")
assertString(t, opts.Group.Group.Opt, "c")
for _, name := range []string{"Subgroup", "Subsubgroup"} {
if p.Command.Group.Find(name) == nil {
t.Errorf("Expected to find group '%s'", name)
}
}
}
func TestDuplicateShortFlags(t *testing.T) {
var opts struct {
Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"`
Variables []string `short:"v" long:"variable" description:"Set a variable value."`
}
args := []string{
"--verbose",
"-v", "123",
"-v", "456",
}
_, err := ParseArgs(&opts, args)
if err == nil {
t.Errorf("Expected an error with type ErrDuplicatedFlag")
} else {
err2 := err.(*Error)
if err2.Type != ErrDuplicatedFlag {
t.Errorf("Expected an error with type ErrDuplicatedFlag")
}
}
}
func TestDuplicateLongFlags(t *testing.T) {
var opts struct {
Test1 []bool `short:"a" long:"testing" description:"Test 1"`
Test2 []string `short:"b" long:"testing" description:"Test 2."`
}
args := []string{
"--testing",
}
_, err := ParseArgs(&opts, args)
if err == nil {
t.Errorf("Expected an error with type ErrDuplicatedFlag")
} else {
err2 := err.(*Error)
if err2.Type != ErrDuplicatedFlag {
t.Errorf("Expected an error with type ErrDuplicatedFlag")
}
}
}
-426
View File
@@ -1,426 +0,0 @@
// Copyright 2012 Jesse van den Kieboom. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package flags
import (
"bufio"
"bytes"
"fmt"
"io"
"reflect"
"runtime"
"strings"
"unicode/utf8"
)
type alignmentInfo struct {
maxLongLen int
hasShort bool
hasValueName bool
terminalColumns int
indent bool
}
const (
paddingBeforeOption = 2
distanceBetweenOptionAndDescription = 2
)
func (a *alignmentInfo) descriptionStart() int {
ret := a.maxLongLen + distanceBetweenOptionAndDescription
if a.hasShort {
ret += 2
}
if a.maxLongLen > 0 {
ret += 4
}
if a.hasValueName {
ret += 3
}
return ret
}
func (a *alignmentInfo) updateLen(name string, indent bool) {
l := utf8.RuneCountInString(name)
if indent {
l = l + 4
}
if l > a.maxLongLen {
a.maxLongLen = l
}
}
func (p *Parser) getAlignmentInfo() alignmentInfo {
ret := alignmentInfo{
maxLongLen: 0,
hasShort: false,
hasValueName: false,
terminalColumns: getTerminalColumns(),
}
if ret.terminalColumns <= 0 {
ret.terminalColumns = 80
}
var prevcmd *Command
p.eachActiveGroup(func(c *Command, grp *Group) {
if c != prevcmd {
for _, arg := range c.args {
ret.updateLen(arg.Name, c != p.Command)
}
}
for _, info := range grp.options {
if !info.canCli() {
continue
}
if info.ShortName != 0 {
ret.hasShort = true
}
if len(info.ValueName) > 0 {
ret.hasValueName = true
}
ret.updateLen(info.LongNameWithNamespace()+info.ValueName, c != p.Command)
}
})
return ret
}
func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alignmentInfo) {
line := &bytes.Buffer{}
prefix := paddingBeforeOption
if info.indent {
prefix += 4
}
line.WriteString(strings.Repeat(" ", prefix))
if option.ShortName != 0 {
line.WriteRune(defaultShortOptDelimiter)
line.WriteRune(option.ShortName)
} else if info.hasShort {
line.WriteString(" ")
}
descstart := info.descriptionStart() + paddingBeforeOption
if len(option.LongName) > 0 {
if option.ShortName != 0 {
line.WriteString(", ")
} else if info.hasShort {
line.WriteString(" ")
}
line.WriteString(defaultLongOptDelimiter)
line.WriteString(option.LongNameWithNamespace())
}
if option.canArgument() {
line.WriteRune(defaultNameArgDelimiter)
if len(option.ValueName) > 0 {
line.WriteString(option.ValueName)
}
}
written := line.Len()
line.WriteTo(writer)
if option.Description != "" {
dw := descstart - written
writer.WriteString(strings.Repeat(" ", dw))
def := ""
defs := option.Default
if len(option.DefaultMask) != 0 {
if option.DefaultMask != "-" {
def = option.DefaultMask
}
} else if len(defs) == 0 && option.canArgument() {
var showdef bool
switch option.field.Type.Kind() {
case reflect.Func, reflect.Ptr:
showdef = !option.value.IsNil()
case reflect.Slice, reflect.String, reflect.Array:
showdef = option.value.Len() > 0
case reflect.Map:
showdef = !option.value.IsNil() && option.value.Len() > 0
default:
zeroval := reflect.Zero(option.field.Type)
showdef = !reflect.DeepEqual(zeroval.Interface(), option.value.Interface())
}
if showdef {
def, _ = convertToString(option.value, option.tag)
}
} else if len(defs) != 0 {
l := len(defs) - 1
for i := 0; i < l; i++ {
def += quoteIfNeeded(defs[i]) + ", "
}
def += quoteIfNeeded(defs[l])
}
var envDef string
if option.EnvDefaultKey != "" {
var envPrintable string
if runtime.GOOS == "windows" {
envPrintable = "%" + option.EnvDefaultKey + "%"
} else {
envPrintable = "$" + option.EnvDefaultKey
}
envDef = fmt.Sprintf(" [%s]", envPrintable)
}
var desc string
if def != "" {
desc = fmt.Sprintf("%s (%v)%s", option.Description, def, envDef)
} else {
desc = option.Description + envDef
}
writer.WriteString(wrapText(desc,
info.terminalColumns-descstart,
strings.Repeat(" ", descstart)))
}
writer.WriteString("\n")
}
func maxCommandLength(s []*Command) int {
if len(s) == 0 {
return 0
}
ret := len(s[0].Name)
for _, v := range s[1:] {
l := len(v.Name)
if l > ret {
ret = l
}
}
return ret
}
// WriteHelp writes a help message containing all the possible options and
// their descriptions to the provided writer. Note that the HelpFlag parser
// option provides a convenient way to add a -h/--help option group to the
// command line parser which will automatically show the help messages using
// this method.
func (p *Parser) WriteHelp(writer io.Writer) {
if writer == nil {
return
}
wr := bufio.NewWriter(writer)
aligninfo := p.getAlignmentInfo()
cmd := p.Command
for cmd.Active != nil {
cmd = cmd.Active
}
if p.Name != "" {
wr.WriteString("Usage:\n")
wr.WriteString(" ")
allcmd := p.Command
for allcmd != nil {
var usage string
if allcmd == p.Command {
if len(p.Usage) != 0 {
usage = p.Usage
} else if p.Options&HelpFlag != 0 {
usage = "[OPTIONS]"
}
} else if us, ok := allcmd.data.(Usage); ok {
usage = us.Usage()
} else if allcmd.hasCliOptions() {
usage = fmt.Sprintf("[%s-OPTIONS]", allcmd.Name)
}
if len(usage) != 0 {
fmt.Fprintf(wr, " %s %s", allcmd.Name, usage)
} else {
fmt.Fprintf(wr, " %s", allcmd.Name)
}
if len(allcmd.args) > 0 {
fmt.Fprintf(wr, " ")
}
for i, arg := range allcmd.args {
if i != 0 {
fmt.Fprintf(wr, " ")
}
name := arg.Name
if arg.isRemaining() {
name = name + "..."
}
if !allcmd.ArgsRequired {
fmt.Fprintf(wr, "[%s]", name)
} else {
fmt.Fprintf(wr, "%s", name)
}
}
if allcmd.Active == nil && len(allcmd.commands) > 0 {
var co, cc string
if allcmd.SubcommandsOptional {
co, cc = "[", "]"
} else {
co, cc = "<", ">"
}
if len(allcmd.commands) > 3 {
fmt.Fprintf(wr, " %scommand%s", co, cc)
} else {
subcommands := allcmd.sortedCommands()
names := make([]string, len(subcommands))
for i, subc := range subcommands {
names[i] = subc.Name
}
fmt.Fprintf(wr, " %s%s%s", co, strings.Join(names, " | "), cc)
}
}
allcmd = allcmd.Active
}
fmt.Fprintln(wr)
if len(cmd.LongDescription) != 0 {
fmt.Fprintln(wr)
t := wrapText(cmd.LongDescription,
aligninfo.terminalColumns,
"")
fmt.Fprintln(wr, t)
}
}
c := p.Command
for c != nil {
printcmd := c != p.Command
c.eachGroup(func(grp *Group) {
first := true
// Skip built-in help group for all commands except the top-level
// parser
if grp.isBuiltinHelp && c != p.Command {
return
}
for _, info := range grp.options {
if !info.canCli() {
continue
}
if printcmd {
fmt.Fprintf(wr, "\n[%s command options]\n", c.Name)
aligninfo.indent = true
printcmd = false
}
if first && cmd.Group != grp {
fmt.Fprintln(wr)
if aligninfo.indent {
wr.WriteString(" ")
}
fmt.Fprintf(wr, "%s:\n", grp.ShortDescription)
first = false
}
p.writeHelpOption(wr, info, aligninfo)
}
})
if len(c.args) > 0 {
if c == p.Command {
fmt.Fprintf(wr, "\nArguments:\n")
} else {
fmt.Fprintf(wr, "\n[%s command arguments]\n", c.Name)
}
maxlen := aligninfo.descriptionStart()
for _, arg := range c.args {
prefix := strings.Repeat(" ", paddingBeforeOption)
fmt.Fprintf(wr, "%s%s", prefix, arg.Name)
if len(arg.Description) > 0 {
align := strings.Repeat(" ", maxlen-len(arg.Name)-1)
fmt.Fprintf(wr, ":%s%s", align, arg.Description)
}
fmt.Fprintln(wr)
}
}
c = c.Active
}
scommands := cmd.sortedCommands()
if len(scommands) > 0 {
maxnamelen := maxCommandLength(scommands)
fmt.Fprintln(wr)
fmt.Fprintln(wr, "Available commands:")
for _, c := range scommands {
fmt.Fprintf(wr, " %s", c.Name)
if len(c.ShortDescription) > 0 {
pad := strings.Repeat(" ", maxnamelen-len(c.Name))
fmt.Fprintf(wr, "%s %s", pad, c.ShortDescription)
if len(c.Aliases) > 0 {
fmt.Fprintf(wr, " (aliases: %s)", strings.Join(c.Aliases, ", "))
}
}
fmt.Fprintln(wr)
}
}
wr.Flush()
}
-283
View File
@@ -1,283 +0,0 @@
package flags
import (
"bytes"
"fmt"
"os"
"runtime"
"testing"
"time"
)
type helpOptions struct {
Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information" ini-name:"verbose"`
Call func(string) `short:"c" description:"Call phone number" ini-name:"call"`
PtrSlice []*string `long:"ptrslice" description:"A slice of pointers to string"`
EmptyDescription bool `long:"empty-description"`
Default string `long:"default" default:"Some\nvalue" description:"Test default value"`
DefaultArray []string `long:"default-array" default:"Some value" default:"Other\tvalue" description:"Test default array value"`
DefaultMap map[string]string `long:"default-map" default:"some:value" default:"another:value" description:"Testdefault map value"`
EnvDefault1 string `long:"env-default1" default:"Some value" env:"ENV_DEFAULT" description:"Test env-default1 value"`
EnvDefault2 string `long:"env-default2" env:"ENV_DEFAULT" description:"Test env-default2 value"`
OnlyIni string `ini-name:"only-ini" description:"Option only available in ini"`
Other struct {
StringSlice []string `short:"s" default:"some" default:"value" description:"A slice of strings"`
IntMap map[string]int `long:"intmap" default:"a:1" description:"A map from string to int" ini-name:"int-map"`
} `group:"Other Options"`
Group struct {
Opt string `long:"opt" description:"This is a subgroup option"`
Group struct {
Opt string `long:"opt" description:"This is a subsubgroup option"`
} `group:"Subsubgroup" namespace:"sap"`
} `group:"Subgroup" namespace:"sip"`
Command struct {
ExtraVerbose []bool `long:"extra-verbose" description:"Use for extra verbosity"`
} `command:"command" alias:"cm" alias:"cmd" description:"A command"`
Args struct {
Filename string `name:"filename" description:"A filename"`
Num int `name:"num" description:"A number"`
} `positional-args:"yes"`
}
func TestHelp(t *testing.T) {
oldEnv := EnvSnapshot()
defer oldEnv.Restore()
os.Setenv("ENV_DEFAULT", "env-def")
var opts helpOptions
p := NewNamedParser("TestHelp", HelpFlag)
p.AddGroup("Application Options", "The application options", &opts)
_, err := p.ParseArgs([]string{"--help"})
if err == nil {
t.Fatalf("Expected help error")
}
if e, ok := err.(*Error); !ok {
t.Fatalf("Expected flags.Error, but got %T", err)
} else {
if e.Type != ErrHelp {
t.Errorf("Expected flags.ErrHelp type, but got %s", e.Type)
}
var expected string
if runtime.GOOS == "windows" {
expected = `Usage:
TestHelp [OPTIONS] [filename] [num] <command>
Application Options:
/v, /verbose Show verbose debug information
/c: Call phone number
/ptrslice: A slice of pointers to string
/empty-description
/default: Test default value ("Some\nvalue")
/default-array: Test default array value (Some value, "Other\tvalue")
/default-map: Testdefault map value (some:value, another:value)
/env-default1: Test env-default1 value (Some value) [%ENV_DEFAULT%]
/env-default2: Test env-default2 value [%ENV_DEFAULT%]
Other Options:
/s: A slice of strings (some, value)
/intmap: A map from string to int (a:1)
Subgroup:
/sip.opt: This is a subgroup option
Subsubgroup:
/sip.sap.opt: This is a subsubgroup option
Help Options:
/? Show this help message
/h, /help Show this help message
Arguments:
filename: A filename
num: A number
Available commands:
command A command (aliases: cm, cmd)
`
} else {
expected = `Usage:
TestHelp [OPTIONS] [filename] [num] <command>
Application Options:
-v, --verbose Show verbose debug information
-c= Call phone number
--ptrslice= A slice of pointers to string
--empty-description
--default= Test default value ("Some\nvalue")
--default-array= Test default array value (Some value, "Other\tvalue")
--default-map= Testdefault map value (some:value, another:value)
--env-default1= Test env-default1 value (Some value) [$ENV_DEFAULT]
--env-default2= Test env-default2 value [$ENV_DEFAULT]
Other Options:
-s= A slice of strings (some, value)
--intmap= A map from string to int (a:1)
Subgroup:
--sip.opt= This is a subgroup option
Subsubgroup:
--sip.sap.opt= This is a subsubgroup option
Help Options:
-h, --help Show this help message
Arguments:
filename: A filename
num: A number
Available commands:
command A command (aliases: cm, cmd)
`
}
assertDiff(t, e.Message, expected, "help message")
}
}
func TestMan(t *testing.T) {
oldEnv := EnvSnapshot()
defer oldEnv.Restore()
os.Setenv("ENV_DEFAULT", "env-def")
var opts helpOptions
p := NewNamedParser("TestMan", HelpFlag)
p.ShortDescription = "Test manpage generation"
p.LongDescription = "This is a somewhat `longer' description of what this does"
p.AddGroup("Application Options", "The application options", &opts)
p.Commands()[0].LongDescription = "Longer `command' description"
var buf bytes.Buffer
p.WriteManPage(&buf)
got := buf.String()
tt := time.Now()
expected := fmt.Sprintf(`.TH TestMan 1 "%s"
.SH NAME
TestMan \- Test manpage generation
.SH SYNOPSIS
\fBTestMan\fP [OPTIONS]
.SH DESCRIPTION
This is a somewhat \fBlonger\fP description of what this does
.SH OPTIONS
.TP
\fB-v, --verbose\fP
Show verbose debug information
.TP
\fB-c\fP
Call phone number
.TP
\fB--ptrslice\fP
A slice of pointers to string
.TP
\fB--empty-description\fP
.TP
\fB--default\fP
Test default value
.TP
\fB--default-array\fP
Test default array value
.TP
\fB--default-map\fP
Testdefault map value
.TP
\fB--env-default1\fP
Test env-default1 value
.TP
\fB--env-default2\fP
Test env-default2 value
.TP
\fB-s\fP
A slice of strings
.TP
\fB--intmap\fP
A map from string to int
.TP
\fB--sip.opt\fP
This is a subgroup option
.TP
\fB--sip.sap.opt\fP
This is a subsubgroup option
.SH COMMANDS
.SS command
A command
Longer \fBcommand\fP description
\fBUsage\fP: TestMan [OPTIONS] command [command-OPTIONS]
\fBAliases\fP: cm, cmd
.TP
\fB--extra-verbose\fP
Use for extra verbosity
`, tt.Format("2 January 2006"))
assertDiff(t, got, expected, "man page")
}
type helpCommandNoOptions struct {
Command struct {
} `command:"command" description:"A command"`
}
func TestHelpCommand(t *testing.T) {
oldEnv := EnvSnapshot()
defer oldEnv.Restore()
os.Setenv("ENV_DEFAULT", "env-def")
var opts helpCommandNoOptions
p := NewNamedParser("TestHelpCommand", HelpFlag)
p.AddGroup("Application Options", "The application options", &opts)
_, err := p.ParseArgs([]string{"command", "--help"})
if err == nil {
t.Fatalf("Expected help error")
}
if e, ok := err.(*Error); !ok {
t.Fatalf("Expected flags.Error, but got %T", err)
} else {
if e.Type != ErrHelp {
t.Errorf("Expected flags.ErrHelp type, but got %s", e.Type)
}
var expected string
if runtime.GOOS == "windows" {
expected = `Usage:
TestHelpCommand [OPTIONS] command
Help Options:
/? Show this help message
/h, /help Show this help message
`
} else {
expected = `Usage:
TestHelpCommand [OPTIONS] command
Help Options:
-h, --help Show this help message
`
}
assertDiff(t, e.Message, expected, "help message")
}
}
-140
View File
@@ -1,140 +0,0 @@
package flags
import (
"fmt"
"io"
)
// IniError contains location information on where an error occured.
type IniError struct {
// The error message.
Message string
// The filename of the file in which the error occurred.
File string
// The line number at which the error occurred.
LineNumber uint
}
// Error provides a "file:line: message" formatted message of the ini error.
func (x *IniError) Error() string {
return fmt.Sprintf(
"%s:%d: %s",
x.File,
x.LineNumber,
x.Message,
)
}
// IniOptions for writing
type IniOptions uint
const (
// IniNone indicates no options.
IniNone IniOptions = 0
// IniIncludeDefaults indicates that default values should be written.
IniIncludeDefaults = 1 << iota
// IniCommentDefaults indicates that if IniIncludeDefaults is used
// options with default values are written but commented out.
IniCommentDefaults
// IniIncludeComments indicates that comments containing the description
// of an option should be written.
IniIncludeComments
// IniDefault provides a default set of options.
IniDefault = IniIncludeComments
)
// IniParser is a utility to read and write flags options from and to ini
// formatted strings.
type IniParser struct {
parser *Parser
}
// NewIniParser creates a new ini parser for a given Parser.
func NewIniParser(p *Parser) *IniParser {
return &IniParser{
parser: p,
}
}
// IniParse is a convenience function to parse command line options with default
// settings from an ini formatted file. The provided data is a pointer to a struct
// representing the default option group (named "Application Options"). For
// more control, use flags.NewParser.
func IniParse(filename string, data interface{}) error {
p := NewParser(data, Default)
return NewIniParser(p).ParseFile(filename)
}
// ParseFile parses flags from an ini formatted file. See Parse for more
// information on the ini file format. The returned errors can be of the type
// flags.Error or flags.IniError.
func (i *IniParser) ParseFile(filename string) error {
i.parser.clearIsSet()
ini, err := readIniFromFile(filename)
if err != nil {
return err
}
return i.parse(ini)
}
// Parse parses flags from an ini format. You can use ParseFile as a
// convenience function to parse from a filename instead of a general
// io.Reader.
//
// The format of the ini file is as follows:
//
// [Option group name]
// option = value
//
// Each section in the ini file represents an option group or command in the
// flags parser. The default flags parser option group (i.e. when using
// flags.Parse) is named 'Application Options'. The ini option name is matched
// in the following order:
//
// 1. Compared to the ini-name tag on the option struct field (if present)
// 2. Compared to the struct field name
// 3. Compared to the option long name (if present)
// 4. Compared to the option short name (if present)
//
// Sections for nested groups and commands can be addressed using a dot `.'
// namespacing notation (i.e [subcommand.Options]). Group section names are
// matched case insensitive.
//
// The returned errors can be of the type flags.Error or flags.IniError.
func (i *IniParser) Parse(reader io.Reader) error {
i.parser.clearIsSet()
ini, err := readIni(reader, "")
if err != nil {
return err
}
return i.parse(ini)
}
// WriteFile writes the flags as ini format into a file. See WriteIni
// for more information. The returned error occurs when the specified file
// could not be opened for writing.
func (i *IniParser) WriteFile(filename string, options IniOptions) error {
return writeIniToFile(i, filename, options)
}
// Write writes the current values of all the flags to an ini format.
// See Parse for more information on the ini file format. You typically
// call this only after settings have been parsed since the default values of each
// option are stored just before parsing the flags (this is only relevant when
// IniIncludeDefaults is _not_ set in options).
func (i *IniParser) Write(writer io.Writer, options IniOptions) {
writeIni(i, writer, options)
}

Some files were not shown because too many files have changed in this diff Show More